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/.gitmodules b/.gitmodules index f247e5c69e..64a98a2f94 100644 --- a/.gitmodules +++ b/.gitmodules @@ -49,3 +49,6 @@ [submodule "3rdparty/ZArchive"] path = 3rdparty/ZArchive url = https://github.com/Exzap/ZArchive.git +[submodule "3rdparty/vgmstream"] + path = 3rdparty/vgmstream + url = https://github.com/vgmstream/vgmstream.git diff --git a/3rdparty/vgmstream b/3rdparty/vgmstream new file mode 160000 index 0000000000..7c520e8f9b --- /dev/null +++ b/3rdparty/vgmstream @@ -0,0 +1 @@ +Subproject commit 7c520e8f9b078dfde2f84f1b5bcbad0c6c415806 diff --git a/CMakeLists.txt b/CMakeLists.txt index 82ee188f83..9eaa66c8db 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}") @@ -62,6 +65,18 @@ include(cmake/SystemDeps.cmake) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) +include(CheckCXXSourceCompiles) +function(anyps5_check_cxx_links_standard_library) + set(CMAKE_TRY_COMPILE_TARGET_TYPE EXECUTABLE) + check_cxx_source_compiles("#include \nint main(int argc, char**) { try { throw std::string(static_cast(argc), 'x'); } catch (const std::string& text) { return static_cast(text.size()) - argc; } }" ANYPS5_CXX_LINKS_STANDARD_LIBRARY) +endfunction() +anyps5_check_cxx_links_standard_library() +if(NOT ANYPS5_CXX_LINKS_STANDARD_LIBRARY) + message(FATAL_ERROR "The C++ compiler ${CMAKE_CXX_COMPILER} cannot link a program that uses the C++ standard library. " + "Set CMAKE_CXX_COMPILER to the C++ driver (g++ or clang++, not gcc or clang) in a new build directory. " + "Supported toolchains are listed in docs/dev/BUILD.md.") +endif() + if(ANYPS5_ENABLE_ASAN) if(NOT CMAKE_SYSTEM_NAME STREQUAL "Linux") message(FATAL_ERROR "ANYPS5_ENABLE_ASAN is supported only on Linux") @@ -108,6 +123,10 @@ if(BUILD_TESTING) core/shader/recompiler/IntermediateRepresentation/include) add_test(NAME shader_ir_use_rewrite COMMAND shader_ir_use_rewrite_tests) set_tests_properties(shader_ir_use_rewrite PROPERTIES TIMEOUT 20) + add_test_executable(pad_command_file_tests core/libs/prx/libScePad/tests/InputCommandFile.cpp) + target_include_directories(pad_command_file_tests PRIVATE core/libs) + add_test(NAME pad_command_file COMMAND pad_command_file_tests) + set_tests_properties(pad_command_file PROPERTIES TIMEOUT 20) if(Python3_Interpreter_FOUND) add_test(NAME shader_cache_native_paths COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/tests/test_shader_cache_native_paths.py @@ -155,6 +174,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} @@ -163,6 +184,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_debt_length.py) add_test(NAME test_registration COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_test_registration.py) + add_test(NAME test_script_registration COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/check_test_registration.py --scripts) add_test(NAME build_eligibility COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_build_eligibility.py) add_test(NAME check_skips COMMAND ${Python3_EXECUTABLE} @@ -172,7 +195,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} @@ -183,23 +212,31 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/hw-oracle/test_hw_oracle.py) add_test(NAME hardware_oracle_compare COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/hw-oracle/test_compare_results.py) - set_tests_properties(hardware_oracle_compare PROPERTIES TIMEOUT 20) + set_tests_properties(hardware_oracle_compare PROPERTIES TIMEOUT 120) add_test(NAME pr_overlap_exports COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_exports.py) add_test(NAME pr_overlap_wrapped_exports COMMAND ${Python3_EXECUTABLE} ${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) add_test(NAME pad_input COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pad_input.py) set_tests_properties(pad_input PROPERTIES TIMEOUT 20) + add_test(NAME pad_timeline COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pad_timeline.py) + set_tests_properties(pad_timeline PROPERTIES TIMEOUT 20) add_test(NAME trace_digest COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_trace_digest.py) add_test(NAME conventions_bom_shebang COMMAND ${Python3_EXECUTABLE} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8394d4559a..f6668eb80b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,10 +5,11 @@ - Follow the [coding conventions](docs/dev/CONVENTIONS.md): naming, no comments except [technical debt](docs/dev/TechnicalDebt.md), Conventional Commits. - Every function either does exactly what it is supposed to or throws. Unimplemented exports call `NotImplemented_nid_no_patch(__func__)` (see [libSceAudioIn](core/libs/prx/libSceAudioIn/Export.cpp)). - Pull requests that add or change shader instruction semantics say where they come from: measured on hardware (which GPU and what was checked, e.g. with the [hardware oracle](docs/dev/HW_ORACLE.md)) or the exact source (ISA section, LLVM, ACO or Mesa file). Reviewers check the semantics on hardware. Cases the source doesn't settle throw, and behaviour not verified on hardware is recorded in [technical debt](docs/dev/TechnicalDebt.md). +- An export's signature or behaviour taken from a title's call sites or from a library's disassembly follows [reading titles and libraries](docs/dev/REVERSE_ENGINEERING.md): a call site settles the arguments, not what the library does. - A silent stub is allowed only when it unblocks a title and only affects the UI; add it under Silent stubs in its library's [technical debt](docs/dev/debt/) file. - Missing imports fail at startup with a clear error; don't replace them with fallbacks that keep running. - Implement the general behaviour of a function, not what one title happens to need. -- Don't add replacements for modules that titles ship themselves in `sce_module/`, `sce_modules/` or `prx/`: engine or middleware modules (Cohtml, FMOD, GOG Galaxy) and SDK libraries that only wrap other system libraries (NpCppWebApi over NpWebApi2). The relinker converts and loads the title's own module, and a host library with the same name is left out of `DT_NEEDED`. Only system libraries, which reach the kernel or the hardware, are reimplemented in [core/libs/prx](core/libs/prx). +- Don't add replacements for modules that titles ship themselves in `sce_module/`, `sce_modules/` or `prx/`: engine or middleware modules (Cohtml, FMOD, GOG Galaxy) and SDK libraries that only wrap other system libraries. The relinker converts and loads the title's own module, and a host library with the same name is left out of `DT_NEEDED`. Only system libraries, which reach the kernel or the hardware, are reimplemented in [core/libs/prx](core/libs/prx). A host copy may still exist for a wrapped library that some dumps lack entirely, like libSceNpCppWebApi: the title's own module wins whenever one is shipped, so it is only ever consulted by titles that ship nothing. - Avoid non-standard extensions (`__attribute__`, etc.) where standard C++ is enough. Helper symbols that must not become NIDs use the `_nid_no_patch` or `_nid_no_patch_cut` suffix. - The relinker uses only the C++20 standard library. - Third-party code is added as a submodule under `3rdparty/` and built from source, not found on the system. With `ANYPS5_USE_SYSTEM_DEPS=ON` the packages packaged by the distributions (SDL2, freetype, Vulkan-Headers, SPIRV-Headers, glslang and SPIRV-Tools) come from the system instead; that resolution lives in [cmake/SystemDeps.cmake](cmake/SystemDeps.cmake) and the default build never reads the system. 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 8c37318b57..2854a5fa08 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -82,6 +82,9 @@ add_library(libc SHARED EXCLUDE_FROM_ALL) set(LIBS_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}) add_subdirectory(${LIBC_DIR} ${CMAKE_CURRENT_BINARY_DIR}/prx/libc) target_include_directories(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) +if(WIN32) + target_compile_options(libc PRIVATE "SHELL:-include ${CMAKE_CURRENT_SOURCE_DIR}/prx/libc/include/HostStandardStreams.hpp") +endif() configure_windows_unwind(libc) if(CMAKE_SYSTEM_NAME STREQUAL "Linux") target_link_options(libc PRIVATE LINKER:-z,now) @@ -139,6 +142,9 @@ foreach(LIB_DIR ${LIBS_DIRS}) configure_windows_unwind(${LIB_NAME}) endif() + if(WIN32) + target_compile_options(${LIB_NAME} PRIVATE "SHELL:-include ${CMAKE_CURRENT_SOURCE_DIR}/prx/libc/include/HostStandardStreams.hpp") + endif() if(LIB_NAME STREQUAL "libkernel") target_link_libraries(${LIB_NAME} PUBLIC libc) else() @@ -354,6 +360,26 @@ 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_np_party_tests tests/GuestNpParty.cpp) + target_include_directories(guest_np_party_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_np_party_tests PRIVATE libSceNpParty libc) + configure_windows_unwind(guest_np_party_tests) + add_test(NAME guest_np_party COMMAND guest_np_party_tests) + if(WIN32) + set_tests_properties(guest_np_party 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 +424,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 +494,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) @@ -537,6 +583,16 @@ if(BUILD_TESTING) set_tests_properties(prx_thread_local_destructors PROPERTIES TIMEOUT 300) endif() + add_test_executable(guest_np_cpp_web_api_intrusive_ptr_tests tests/GuestNpCppWebApiIntrusivePtr.cpp) + target_include_directories(guest_np_cpp_web_api_intrusive_ptr_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_np_cpp_web_api_intrusive_ptr_tests PRIVATE libSceNpCppWebApi libc) + configure_windows_unwind(guest_np_cpp_web_api_intrusive_ptr_tests) + add_test(NAME guest_np_cpp_web_api_intrusive_ptr COMMAND guest_np_cpp_web_api_intrusive_ptr_tests) + if(WIN32) + set_tests_properties(guest_np_cpp_web_api_intrusive_ptr PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(savedata_core_write_replacement_tests tests/SaveDataWriteReplacement.cpp) target_include_directories(savedata_core_write_replacement_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(savedata_core_write_replacement_tests PRIVATE libSceSaveData) diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index d46f23177a..4f8d8e8fd7 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; @@ -1460,6 +1460,26 @@ struct NpWebApi2ResponseInformationOption { std::size_t response_data_size; }; +struct NpWebApi2IntInitializeArgs { + std::int32_t lib_http_ctx_id; + std::int32_t reserved; + std::uint64_t pool_size; + const char* name; + std::uint64_t struct_size; +}; +static_assert(sizeof(NpWebApi2IntInitializeArgs) == 32); + +struct NpWebApi2IntInitialize2Args { + std::int32_t lib_http_ctx_id; + std::int32_t reserved; + std::uint64_t pool_size; + const char* name; + std::uint32_t push_config_group; + std::int32_t reserved2; + std::uint64_t struct_size; +}; +static_assert(sizeof(NpWebApi2IntInitialize2Args) == 40); + struct GameUpdateCheckParam { std::size_t size; std::uint32_t option; @@ -1901,6 +1921,15 @@ struct SystemServiceDisplaySafeAreaInfo { std::uint8_t reserved[128]; }; +struct SystemServicePlayerDialogParam { + std::uint64_t size; + std::int32_t mode; + std::int32_t user_id; + std::uint64_t target_account_id; + std::uint8_t reserved[40]; +}; +static_assert(sizeof(SystemServicePlayerDialogParam) == 0x40); + struct SystemServiceHdrToneMapLuminance { float max_full_frame_tone_map_luminance; float max_tone_map_luminance; @@ -1941,6 +1970,13 @@ struct SystemGestureTouchRecognizerInformation { struct SystemGestureTouchEvent { std::uint8_t reserve[168]; }; +struct SystemGestureTouchPadData { + std::int32_t pad_handle; + std::int32_t report_number; + const PadData* pad_data_buffer; + std::uint8_t reserve[8]; +}; + struct UserServiceLoginUserIdList { int user_id[4]; }; struct UserServiceRegisteredUserIdList { int user_id[16]; }; diff --git a/core/libs/prx/libSceAcm/Export.cpp b/core/libs/prx/libSceAcm/Export.cpp index 97dfc19d91..325afaa575 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,20 @@ 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; +} + + +int APS5_VABI sceAcmBatchJobNotification() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceAgc/Acb/src/Control.cpp b/core/libs/prx/libSceAgc/Acb/src/Control.cpp index 0eb8d4fd17..567bfd5e18 100644 --- a/core/libs/prx/libSceAgc/Acb/src/Control.cpp +++ b/core/libs/prx/libSceAgc/Acb/src/Control.cpp @@ -72,13 +72,14 @@ std::uint32_t* APS5_VABI sceAgcAcbWaitUntilSafeForRendering(CommandBuffer* buf, } std::uint32_t* APS5_VABI sceAgcAcbSetFlip(CommandBuffer* buf, std::uint32_t videoOutHandle, std::int32_t displayBufferIndex, std::uint32_t flipMode, std::int64_t flipArg) { - (void)buf; - (void)videoOutHandle; - (void)displayBufferIndex; - (void)flipMode; - (void)flipArg; - NotImplemented_nid_no_patch(__func__); - return nullptr; + auto* packet = Agc::Command::Allocate(buf, AgcDriver::FlipPacketWords, __func__); + packet[0] = AgcDriver::FlipPacketHeader; + packet[1] = videoOutHandle; + packet[2] = static_cast(displayBufferIndex); + packet[3] = flipMode; + packet[4] = static_cast(static_cast(flipArg)); + packet[5] = static_cast(static_cast(flipArg) >> 32u); + return packet; } std::uint32_t* APS5_VABI sceAgcAcbSetWorkloadsActive(CommandBuffer* buf, std::uint32_t streamId, const std::uint32_t* workloadIds, std::uint32_t workloadCount) { 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/DcbDraw/src/IndexBuffer.cpp b/core/libs/prx/libSceAgc/DcbDraw/src/IndexBuffer.cpp index f304439f4c..1d2414ae38 100644 --- a/core/libs/prx/libSceAgc/DcbDraw/src/IndexBuffer.cpp +++ b/core/libs/prx/libSceAgc/DcbDraw/src/IndexBuffer.cpp @@ -42,16 +42,14 @@ std::uint32_t* APS5_VABI sceAgcDcbSetIndexSize_0600(CommandBuffer* buf, std::uin return packet; } -std::uint32_t* APS5_VABI sceAgcDcbSetIndexIndirectArgs(CommandBuffer* buf, std::uint32_t dataOffsetInBytes) { - (void)buf; - (void)dataOffsetInBytes; - NotImplemented_nid_no_patch(__func__); - return nullptr; +std::uint32_t* APS5_VABI sceAgcDcbSetIndexIndirectArgs(CommandBuffer* buf, std::uint64_t attributeAddress, std::uint32_t attributeIndex) { + if (attributeAddress != 0) Agc::Command::CheckAddress(attributeAddress, 16, __func__); + Agc::Command::CheckBits(attributeIndex, 0xffffu, __func__); + return Agc::Command::Emit(buf, 0x91u, {static_cast(attributeAddress), static_cast(attributeAddress >> 32u), attributeIndex}, __func__); } std::uint32_t APS5_VABI sceAgcDcbSetIndexIndirectArgsGetSize() { - NotImplemented_nid_no_patch(__func__); - return 0; + return 16; } } diff --git a/core/libs/prx/libSceAgc/Export.cpp b/core/libs/prx/libSceAgc/Export.cpp new file mode 100644 index 0000000000..8e933ec1cf --- /dev/null +++ b/core/libs/prx/libSceAgc/Export.cpp @@ -0,0 +1,13 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceAgcSuspendPointAndCheckStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceAgc/tests/Commands.cpp b/core/libs/prx/libSceAgc/tests/Commands.cpp index 4313f1cd68..c5ac16fa9c 100644 --- a/core/libs/prx/libSceAgc/tests/Commands.cpp +++ b/core/libs/prx/libSceAgc/tests/Commands.cpp @@ -16,6 +16,7 @@ extern "C" std::uint32_t* APS5_VABI sceAgcDcbResetQueue(CommandBuffer* buf, std: extern "C" std::uint32_t* APS5_VABI sceAgcDcbClearState(CommandBuffer* buf, std::uint32_t command); extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetFlip(CommandBuffer* buf, std::uint32_t handle, std::int32_t index, std::uint32_t mode, std::int64_t argument); extern "C" std::uint32_t* APS5_VABI sceAgcAcbWaitUntilSafeForRendering(CommandBuffer* buf, std::uint32_t handle, std::uint32_t index); +extern "C" std::uint32_t* APS5_VABI sceAgcAcbSetFlip(CommandBuffer* buf, std::uint32_t handle, std::int32_t index, std::uint32_t mode, std::int64_t argument); extern "C" int APS5_VABI sceAgcSuspendPoint(); extern "C" int APS5_VABI sceAgcInit(std::uint32_t version); extern "C" void* APS5_VABI sceAgcGetRegisterDefaults(); @@ -47,6 +48,8 @@ extern "C" std::uint32_t* APS5_VABI sceAgcAcbPushMarker(CommandBuffer* buf, cons extern "C" std::uint32_t* APS5_VABI sceAgcAcbPopMarker(CommandBuffer* buf); extern "C" std::uint32_t* APS5_VABI sceAgcAcbSetMarker(CommandBuffer* buf, const char* str, std::uint32_t color); extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetIndexBuffer(CommandBuffer* buf, std::uint64_t indexAddress); +extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetIndexIndirectArgs(CommandBuffer* buf, std::uint64_t attributeAddress, std::uint32_t attributeIndex); +extern "C" std::uint32_t APS5_VABI sceAgcDcbSetIndexIndirectArgsGetSize(); extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetBaseIndirectArgs(CommandBuffer* buf, std::uint32_t shaderType, const volatile void* indirectBaseAddr); extern "C" std::uint32_t* APS5_VABI sceAgcSetNop(CommandBuffer* buf, std::uint32_t sizeDw); extern "C" int APS5_VABI sceAgcDebugRaiseException(std::uint32_t exceptionId); @@ -324,6 +327,20 @@ void testIndexBuffer() { throw std::runtime_error("misaligned index buffer was accepted"); } +void testIndexIndirectArgs() { + Storage storage; + const auto* packet = sceAgcDcbSetIndexIndirectArgs(&storage.buffer, 0x1111111122222220u, 0x4444u); + const std::array expected{0xc0029100u, 0x22222220u, 0x11111111u, 0x4444u}; + check(packet == storage.words.data() && std::equal(expected.begin(), expected.end(), packet), "index indirect args packet differs from the console's"); + check(sceAgcDcbSetIndexIndirectArgsGetSize() == expected.size() * sizeof(std::uint32_t), "index indirect args size differs from its packet"); + const auto* cleared = sceAgcDcbSetIndexIndirectArgs(&storage.buffer, 0, 0); + check(cleared == packet + expected.size() && cleared[0] == expected[0] && cleared[1] == 0 && cleared[2] == 0 && cleared[3] == 0, "null index attribute base was not written as 0"); + const auto before = storage.words; + expectFailure([&] { sceAgcDcbSetIndexIndirectArgs(&storage.buffer, 0x1111111122222228u, 0); }); + expectFailure([&] { sceAgcDcbSetIndexIndirectArgs(&storage.buffer, 0x1111111122222220u, 0x10000u); }); + check(storage.words == before && storage.buffer.cursor_up == cleared + expected.size(), "rejected index indirect args modified the buffer"); +} + void testIndirectBase() { Storage storage; alignas(8) std::uint32_t arguments[4]{}; @@ -426,6 +443,16 @@ void testFlip() { full.buffer.cursor_down = full.words.data() + 3; expectFailure([&] { sceAgcAcbWaitUntilSafeForRendering(&full.buffer, 1, 0); }); check(full.buffer.cursor_up == full.words.data(), "failed compute rendering wait advanced cursor"); + Storage acbFlip; + acbFlip.words.fill(0xdeadbeefu); + auto* computeFlip = sceAgcAcbSetFlip(&acbFlip.buffer, 0xfedcba98u, -2, 0x12345678u, -0x123456789abcdefLL); + check(computeFlip == acbFlip.words.data() && std::equal(expected.begin(), expected.end(), computeFlip), "compute flip packet mismatch"); + check(acbFlip.buffer.cursor_up == computeFlip + 6 && computeFlip[6] == 0xdeadbeefu, "compute flip overran allocation"); + expectFailure([] { sceAgcAcbSetFlip(nullptr, 1, 0, 1, 0); }); + Storage fullFlip; + fullFlip.buffer.cursor_down = fullFlip.words.data() + 5; + expectFailure([&] { sceAgcAcbSetFlip(&fullFlip.buffer, 1, 0, 1, 0); }); + check(fullFlip.buffer.cursor_up == fullFlip.words.data(), "failed compute flip advanced cursor"); check(sceAgcSuspendPoint() == 0, "empty suspend failed"); } @@ -463,8 +490,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() { @@ -773,6 +806,7 @@ int main(int argc, char** argv) { testWorkloads(); testWorkloadStreams(); testIndexBuffer(); + testIndexIndirectArgs(); testIndirectBase(); testContextState(); testFlip(); 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 56eb2d534e..198d4a0865 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -172,6 +172,25 @@ foreach(agcPresentationStage IN ITEMS Vert Frag) ) list(APPEND agcPresentationShaderHeaders ${agcPresentationShaderHeader}) endforeach() +foreach(agcSampleOrderVariant IN ITEMS "R8;comp;-DTEXEL_FORMAT=r8ui" "R16;comp;-DTEXEL_FORMAT=r16ui" "R32;comp;-DTEXEL_FORMAT=r32ui" "RG32;comp;-DTEXEL_FORMAT=rg32ui" "RGBA32;comp;-DTEXEL_FORMAT=rgba32ui" "Vertex;vert;-DPLAIN=1" "Depth;frag;-DPLAIN=1" "StencilExport;frag;-DSTENCIL_EXPORT=1" "StencilBits;frag;-DSTENCIL_BITS=1") + list(GET agcSampleOrderVariant 0 agcSampleOrderName) + list(GET agcSampleOrderVariant 1 agcSampleOrderExtension) + list(GET agcSampleOrderVariant 2 agcSampleOrderDefine) + string(TOUPPER "${agcSampleOrderName}" agcSampleOrderSymbol) + set(agcSampleOrderShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/SampleOrder.${agcSampleOrderExtension}) + set(agcSampleOrderShaderSpv ${agcTextureDetileGeneratedDir}/SampleOrder${agcSampleOrderName}.spv) + set(agcSampleOrderShaderHeader ${agcTextureDetileGeneratedDir}/SampleOrder${agcSampleOrderName}_spv.h) + add_custom_command( + OUTPUT "${agcSampleOrderShaderHeader}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTextureDetileGeneratedDir}" + COMMAND "$" -V --target-env vulkan1.1 ${agcSampleOrderDefine} -o "${agcSampleOrderShaderSpv}" "${agcSampleOrderShaderSource}" + COMMAND ${CMAKE_COMMAND} -DINPUT=${agcSampleOrderShaderSpv} -DOUTPUT=${agcSampleOrderShaderHeader} + -DSYMBOL=SAMPLE_ORDER_${agcSampleOrderSymbol}_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake" + DEPENDS "${agcSampleOrderShaderSource}" glslang-standalone + VERBATIM + ) + list(APPEND agcSampleOrderShaderHeaders ${agcSampleOrderShaderHeader}) +endforeach() foreach(agcOverlayStage IN ITEMS Vert Frag) string(TOLOWER "${agcOverlayStage}" agcOverlayExtension) string(TOUPPER "${agcOverlayStage}" agcOverlaySymbol) @@ -266,6 +285,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 @@ -296,9 +316,11 @@ set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/MultisampleReadback.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Texture.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/PassHazards.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/SampleOrder.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/BarrierBatch.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/UnitShadow.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Sampler.cpp + ${agcSampleOrderShaderHeaders} ${agcTextureDetileShaderHeader} ${agcReadbackHeaders} ${agcCmaskClearShaderHeader} @@ -358,6 +380,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 +390,9 @@ target_link_libraries(libSceAgcDriver PRIVATE if(APS5_ENABLE_TRACY) target_link_libraries(libSceAgcDriver PUBLIC TracyClient) endif() +if(MINGW OR CMAKE_SYSTEM_NAME STREQUAL "Linux") + target_link_options(libSceAgcDriver PRIVATE LINKER:--exclude-libs,$) +endif() if(BUILD_TESTING) add_test_executable(agc_driver_draw_program_trace_tests @@ -643,7 +669,11 @@ if(BUILD_TESTING) add_test(NAME agc_write_tracking COMMAND agc_write_tracking_tests) set_tests_properties(agc_write_tracking PROPERTIES SKIP_RETURN_CODE 77) if(WIN32) - set_tests_properties(agc_write_tracking PROPERTIES ENVIRONMENT_MODIFICATION + add_test(NAME agc_write_tracking_metadata_commit COMMAND agc_write_tracking_tests --metadata-commit) + set_tests_properties(agc_write_tracking_metadata_commit PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + add_test(NAME agc_resident_direct_remap COMMAND agc_write_tracking_tests --resident-direct-remap) + set_tests_properties(agc_resident_direct_remap PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 20) + set_tests_properties(agc_write_tracking agc_write_tracking_metadata_commit agc_resident_direct_remap PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() @@ -685,6 +715,15 @@ if(BUILD_TESTING) add_test(NAME agc_fragment_barycentric COMMAND agc_fragment_barycentric_tests) endif() +add_test_executable(agc_float32_flush_control_tests EXCLUDE_FROM_ALL tests/Float32FlushControl.cpp) +target_include_directories(agc_float32_flush_control_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) +set_target_properties(agc_float32_flush_control_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_float32_flush_control_tests) +if(BUILD_TESTING) + set_target_properties(agc_float32_flush_control_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_float32_flush_control COMMAND agc_float32_flush_control_tests) +endif() + add_test_executable(agc_pipeline_link_optimization_tests EXCLUDE_FROM_ALL tests/PipelineLinkOptimization.cpp) target_include_directories(agc_pipeline_link_optimization_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) set_target_properties(agc_pipeline_link_optimization_tests PROPERTIES CXX_EXTENSIONS OFF) @@ -712,6 +751,21 @@ if(BUILD_TESTING) add_test(NAME agc_draw_inputs_in_place_driver COMMAND agc_draw_inputs_in_place_driver_tests) endif() +add_test_executable(agc_subgroup_barrier_tests EXCLUDE_FROM_ALL tests/SubgroupBarrier.cpp) +target_include_directories(agc_subgroup_barrier_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) +target_link_libraries(agc_subgroup_barrier_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_subgroup_barrier_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_subgroup_barrier_tests) +if(BUILD_TESTING) + set_target_properties(agc_subgroup_barrier_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_subgroup_barrier COMMAND agc_subgroup_barrier_tests) + set_tests_properties(agc_subgroup_barrier PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1") + if(WIN32) + set_tests_properties(agc_subgroup_barrier PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_device_fault_tests EXCLUDE_FROM_ALL tests/DeviceFault.cpp) target_include_directories(agc_device_fault_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) set_target_properties(agc_device_fault_tests PROPERTIES CXX_EXTENSIONS OFF) @@ -789,6 +843,19 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_f32_denorm_preserve_tests EXCLUDE_FROM_ALL tests/F32DenormPreserve.cpp) +target_link_libraries(agc_f32_denorm_preserve_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_f32_denorm_preserve_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_f32_denorm_preserve_tests) +if(BUILD_TESTING) + set_target_properties(agc_f32_denorm_preserve_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_f32_denorm_preserve COMMAND agc_f32_denorm_preserve_tests) + set_tests_properties(agc_f32_denorm_preserve PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1") + if(WIN32) + set_tests_properties(agc_f32_denorm_preserve PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_spirv_branch_targets_tests EXCLUDE_FROM_ALL tests/SpirvBranchTargets.cpp) target_link_libraries(agc_spirv_branch_targets_tests PRIVATE shader_recompiler_agc_driver libc) set_target_properties(agc_spirv_branch_targets_tests PROPERTIES CXX_EXTENSIONS OFF) @@ -802,6 +869,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) @@ -815,6 +895,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) @@ -933,6 +1026,10 @@ add_test_executable(agc_vop1_control_decode_tests EXCLUDE_FROM_ALL tests/Vop1Con target_link_libraries(agc_vop1_control_decode_tests PRIVATE shader_recompiler_agc_driver libc) set_target_properties(agc_vop1_control_decode_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_vop1_control_decode_tests) +add_test_executable(agc_vertex_input_validation_tests EXCLUDE_FROM_ALL tests/VertexInputValidation.cpp) +target_link_libraries(agc_vertex_input_validation_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_vertex_input_validation_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_vertex_input_validation_tests) add_test_executable(agc_vop2_encoding_decode_tests EXCLUDE_FROM_ALL tests/Vop2EncodingDecode.cpp) target_link_libraries(agc_vop2_encoding_decode_tests PRIVATE shader_recompiler_agc_driver libc) set_target_properties(agc_vop2_encoding_decode_tests PROPERTIES CXX_EXTENSIONS OFF) @@ -1104,6 +1201,12 @@ target_link_libraries(agc_formatted_buffer_byte_stores_tests PRIVATE shader_reco set_target_properties(agc_formatted_buffer_byte_stores_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_formatted_buffer_byte_stores_tests) +add_test_executable(agc_mesh_output_budget_tests EXCLUDE_FROM_ALL tests/MeshOutputBudget.cpp) +target_include_directories(agc_mesh_output_budget_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(agc_mesh_output_budget_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_mesh_output_budget_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_mesh_output_budget_tests) + add_test_executable(agc_shader_disk_cache_tests EXCLUDE_FROM_ALL tests/ShaderDiskCache.cpp) target_include_directories(agc_shader_disk_cache_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(agc_shader_disk_cache_tests PRIVATE shader_recompiler_agc_driver libc) @@ -1116,6 +1219,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) @@ -1329,6 +1438,11 @@ if(BUILD_TESTING) set_tests_properties(agc_unchanged_cpu_stamps PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + if(WIN32) + add_test(NAME agc_partial_watch_restoration COMMAND agc_driver_recorder_tests --partial-watch-restoration-only) + set_tests_properties(agc_partial_watch_restoration PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_storage_kept_unit_settle COMMAND agc_driver_recorder_tests --kept-unit-settle-only) set_tests_properties(agc_storage_kept_unit_settle PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) if(WIN32) @@ -1336,6 +1450,12 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() add_test(NAME agc_storage_baseline_settled COMMAND agc_driver_recorder_tests --settled-baseline-only) + add_test(NAME agc_storage_clear_hand_over COMMAND agc_driver_recorder_tests --clear-hand-over-only) + set_tests_properties(agc_storage_clear_hand_over PROPERTIES SKIP_RETURN_CODE 77) + if(WIN32) + set_tests_properties(agc_storage_clear_hand_over PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_storage_integer_clear COMMAND agc_driver_recorder_tests --integer-clear-only) set_tests_properties(agc_storage_integer_clear PROPERTIES SKIP_RETURN_CODE 77) if(WIN32) @@ -1390,8 +1510,20 @@ if(BUILD_TESTING) set_tests_properties(agc_copied_direct_ordering PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_storage_writeback_blocks COMMAND agc_driver_recorder_tests --writeback-blocks-only) + set_tests_properties(agc_storage_writeback_blocks PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30 ENVIRONMENT "APS5_PIN_WAIT_MS=3000") + if(WIN32) + set_tests_properties(agc_storage_writeback_blocks PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + 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) @@ -1515,6 +1647,7 @@ target_include_directories(agc_host_import_retire_tests PRIVATE ${LIBS_INCLUDE_D target_link_libraries(agc_host_import_retire_tests PRIVATE libSceAgcDriver libc) set_target_properties(agc_host_import_retire_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_host_import_retire_tests) +target_sources(agc_host_import_retire_tests PRIVATE tests/HostImportFailure.cpp) if(BUILD_TESTING) set_target_properties(agc_host_import_retire_tests PROPERTIES EXCLUDE_FROM_ALL OFF) add_test(NAME agc_host_import_retire COMMAND agc_host_import_retire_tests) @@ -1724,6 +1857,7 @@ add_test_executable(agc_driver_bda_device_tests EXCLUDE_FROM_ALL tests/BdaShader.cpp tests/BdaExecution.cpp tests/BdaContracts.cpp + tests/HostImportFailure.cpp ) target_include_directories(agc_driver_bda_device_tests PRIVATE @@ -1735,6 +1869,15 @@ target_include_directories(agc_driver_bda_device_tests PRIVATE target_link_libraries(agc_driver_bda_device_tests PRIVATE libSceAgcDriver SDL2-static libc) set_target_properties(agc_driver_bda_device_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_driver_bda_device_tests) +if(BUILD_TESTING) + add_test(NAME agc_driver_host_import_failure_handles COMMAND agc_driver_bda_device_tests --host-import-failures) + set_tests_properties(agc_driver_host_import_failure_handles 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_host_import_failure_handles PROPERTIES + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() +endif() add_test_executable(agc_driver_mesh_tests EXCLUDE_FROM_ALL tests/Mesh.cpp) target_include_directories(agc_driver_mesh_tests PRIVATE @@ -1782,6 +1925,14 @@ foreach(agcExecutionTestName IN LISTS ANYPS5_AGC_EXECUTION_TESTS) message(FATAL_ERROR "ANYPS5_AGC_EXECUTION_TESTS names ${agcExecutionTestName}, which has no source in tests/execution") endif() endforeach() +if(BUILD_TESTING) + add_test(NAME agc_driver_indirect_thread_dimensions_cpu COMMAND agc_driver_indirect_thread_dimensions_tests) + set_tests_properties(agc_driver_indirect_thread_dimensions_cpu PROPERTIES ENVIRONMENT "APS5_NO_GPU_THREAD_DIMENSIONS=1" SKIP_RETURN_CODE 77 TIMEOUT 60) + if(WIN32) + set_tests_properties(agc_driver_indirect_thread_dimensions_cpu PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() if(ANYPS5_ENABLE_SPIRV_TOOLS) target_compile_definitions(agc_driver_stencil_null_feedback_tests PRIVATE ANYPS5_ENABLE_SPIRV_TOOLS=1) target_link_libraries(agc_driver_stencil_null_feedback_tests PRIVATE SPIRV-Tools-static) @@ -1828,10 +1979,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 @@ -1849,7 +2001,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:$") @@ -1920,6 +2072,27 @@ foreach(stage IN ITEMS vert frag) endforeach() target_include_directories(agc_driver_provoking_vertex_tests PRIVATE ${agcTestShaderGeneratedRoot}) +foreach(agcSampleOrderTestVariant IN ITEMS "R8;SampleOrderTexels.comp;-DTEXEL_FORMAT=r8ui" "R16;SampleOrderTexels.comp;-DTEXEL_FORMAT=r16ui" "R32;SampleOrderTexels.comp;-DTEXEL_FORMAT=r32ui" "RG32;SampleOrderTexels.comp;-DTEXEL_FORMAT=rg32ui" "RGBA32;SampleOrderTexels.comp;-DTEXEL_FORMAT=rgba32ui" "FillVertex;SampleOrderFill.vert;-DPLAIN=1" "FillDepth;SampleOrderFill.frag;-DPLAIN=1" "FillStencil;SampleOrderFill.frag;-DSTENCIL=1" "ReadDepth;SampleOrderRead.comp;-DPLAIN=1" "ReadStencil;SampleOrderRead.comp;-DSTENCIL=1" "ReadColor;SampleOrderRead.comp;-DCOLOR=1") + list(GET agcSampleOrderTestVariant 0 agcSampleOrderTestName) + list(GET agcSampleOrderTestVariant 1 agcSampleOrderTestFile) + list(GET agcSampleOrderTestVariant 2 agcSampleOrderTestDefine) + string(TOUPPER "${agcSampleOrderTestName}" agcSampleOrderTestSymbol) + set(agcSampleOrderTestSource ${CMAKE_CURRENT_SOURCE_DIR}/tests/shaders/${agcSampleOrderTestFile}) + set(agcSampleOrderTestSpv ${agcTestShaderGeneratedRoot}/SampleOrderTest${agcSampleOrderTestName}.spv) + set(agcSampleOrderTestHeader ${agcTestShaderGeneratedRoot}/SampleOrderTest${agcSampleOrderTestName}_spv.h) + add_custom_command( + OUTPUT "${agcSampleOrderTestHeader}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTestShaderGeneratedRoot}" + COMMAND "$" -V --target-env vulkan1.1 ${agcSampleOrderTestDefine} -o "${agcSampleOrderTestSpv}" "${agcSampleOrderTestSource}" + COMMAND ${CMAKE_COMMAND} -DINPUT=${agcSampleOrderTestSpv} -DOUTPUT=${agcSampleOrderTestHeader} + -DSYMBOL=SAMPLE_ORDER_TEST_${agcSampleOrderTestSymbol}_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake" + DEPENDS "${agcSampleOrderTestSource}" glslang-standalone + VERBATIM + ) + target_sources(agc_driver_sample_order_tests PRIVATE ${agcSampleOrderTestHeader}) +endforeach() +target_include_directories(agc_driver_sample_order_tests PRIVATE ${agcTestShaderGeneratedRoot}) + target_include_directories(agc_driver_prepared_exports_tests PRIVATE $) target_include_directories(agc_driver_pipeline_journal_tests PRIVATE $) @@ -1929,6 +2102,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 $) @@ -1968,6 +2142,12 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_readonly_resummarize agc_driver_eop_interrupts PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_driver_small_copy_sync COMMAND agc_driver_tests --small-copy-sync) + set_tests_properties(agc_driver_small_copy_sync PROPERTIES TIMEOUT 30 ENVIRONMENT "APS5_COPY_SYNC=1" SKIP_RETURN_CODE 77) + if(WIN32) + set_tests_properties(agc_driver_small_copy_sync PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_shader_mode_register COMMAND agc_shader_mode_register_tests) set_tests_properties(agc_shader_mode_register PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 20) if(WIN32) @@ -2011,7 +2191,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_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_vertex_input_validation_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_pass_hazards_tests agc_barrier_batch_tests agc_shader_disk_cache_tests agc_screenshot_tests agc_shader_device_profile_tests agc_placeholder_images_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests agc_driver_cmask_fast_clear_tests agc_formatted_buffer_once_tests agc_formatted_buffer_byte_stores_tests ${agcExecutionTests} agc_fma_mix_once_tests agc_f16_single_convert_tests) if(agcTest STREQUAL "agc_driver_constant_store_tests" OR agcTest STREQUAL "agc_driver_flat_apertures_tests" OR agcTest STREQUAL "agc_driver_pipeline_optimized_readback_tests") continue() endif() @@ -2024,6 +2204,27 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endforeach() + add_test(NAME agc_driver_guarded_direct_root COMMAND agc_driver_guarded_null_pointer_tests --direct-root-only) + set_tests_properties(agc_driver_guarded_direct_root 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_guarded_direct_root PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test(NAME agc_driver_bda_device_selection_cpu COMMAND agc_driver_bda_device_tests --device-selection-cpu) + set_tests_properties(agc_driver_bda_device_selection_cpu PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_bda_device_selection_cpu PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test(NAME agc_driver_draw_unsupported_buffer_type_skip_refused COMMAND agc_driver_draw_unsupported_buffer_type_tests --skip-refused) + set_tests_properties(agc_driver_draw_unsupported_buffer_type_skip_refused PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_SKIP_REFUSED_DRAWS=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_draw_unsupported_buffer_type_skip_refused PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_driver_sparse_indirect_indices_unreadable COMMAND agc_driver_sparse_indirect_indices_tests --unreadable) set_tests_properties(agc_driver_sparse_indirect_indices agc_driver_sparse_indirect_indices_unreadable PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") @@ -2045,6 +2246,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) @@ -2060,9 +2262,54 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_partially_resident_pending_stores PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_shader_disk_cache_large_file COMMAND agc_shader_disk_cache_tests --large-file) + if(Python3_Interpreter_FOUND) + set(housekeepingModes plain) + if(WIN32) + list(APPEND housekeepingModes extended extended-long device) + else() + list(APPEND housekeepingModes plain-long) + endif() + foreach(housekeepingMode IN LISTS housekeepingModes) + set(housekeepingTest agc_shader_cache_housekeeping_${housekeepingMode}) + add_test(NAME ${housekeepingTest} COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_shader_cache_housekeeping.py + $ ${housekeepingMode}) + set_tests_properties(${housekeepingTest} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(${housekeepingTest} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() + endforeach() + endif() 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) + add_test(NAME agc_shader_disk_cache_host_stderr COMMAND agc_shader_disk_cache_tests --host-stderr) + set_tests_properties(agc_shader_disk_cache_host_stderr PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + 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(Python3_Interpreter_FOUND) + set(prebuiltDiscoveryModes plain relative plain-long) + if(WIN32) + list(APPEND prebuiltDiscoveryModes extended extended-forward device extended-long) + endif() + foreach(prebuiltDiscoveryMode IN LISTS prebuiltDiscoveryModes) + set(prebuiltDiscoveryTest agc_shader_prebuilt_discovery_${prebuiltDiscoveryMode}) + add_test(NAME ${prebuiltDiscoveryTest} COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_shader_cache_prebuilt_discovery.py + $ ${prebuiltDiscoveryMode}) + set_tests_properties(${prebuiltDiscoveryTest} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(${prebuiltDiscoveryTest} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() + endforeach() + endif() + add_test(NAME agc_driver_large_tracked_snapshot COMMAND agc_driver_partially_resident_snapshot_tests --large-tracked) + set_tests_properties(agc_driver_large_tracked_snapshot PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_driver_large_tracked_snapshot PROPERTIES 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) @@ -2094,6 +2341,13 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() set_tests_properties(agc_driver_native_multisample_depth PROPERTIES TIMEOUT 30) + set_tests_properties(agc_driver_sample_order PROPERTIES TIMEOUT 30) + add_test(NAME agc_driver_sample_order_grid_reference COMMAND agc_driver_sample_order_tests --grid-reference) + add_test(NAME agc_driver_sample_order_recipe_depth_locations COMMAND agc_driver_sample_order_tests --recipe-depth-locations) + set_tests_properties(agc_driver_sample_order_grid_reference agc_driver_sample_order_recipe_depth_locations PROPERTIES TIMEOUT 30 ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_sample_order_grid_reference agc_driver_sample_order_recipe_depth_locations PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() add_test(NAME agc_driver_native_depth_retirement COMMAND agc_driver_native_multisample_depth_tests --retirement) set_tests_properties(agc_driver_native_depth_retirement PROPERTIES TIMEOUT 30 ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") add_test(NAME agc_driver_native_depth_clears COMMAND agc_driver_native_multisample_depth_tests --clears) @@ -2105,12 +2359,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) @@ -2305,12 +2564,22 @@ 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) - set_tests_properties(agc_driver_resource_cache_storage_admission agc_driver_resource_cache_oversized_storage_admission agc_driver_resource_cache_storage_clear PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30) + add_test(NAME agc_driver_resource_cache_replaced_texture COMMAND agc_driver_sampled_cache_budget_tests resource-replaced) + add_test(NAME agc_driver_resource_cache_lookup_changed COMMAND agc_driver_sampled_cache_budget_tests resource-lookup-changed) + set_tests_properties(agc_driver_resource_cache_storage_admission agc_driver_resource_cache_oversized_storage_admission agc_driver_resource_cache_storage_clear agc_driver_resource_cache_replaced_texture agc_driver_resource_cache_lookup_changed PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30) if(WIN32) - set_tests_properties(agc_driver_sampled_cache_budget_memory agc_driver_sampled_cache_budget_without_storage_image_multisample agc_driver_resource_cache_storage_admission agc_driver_resource_cache_oversized_storage_admission agc_driver_resource_cache_storage_clear PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_driver_sampled_cache_budget_memory agc_driver_sampled_cache_budget_without_storage_image_multisample agc_driver_resource_cache_storage_admission agc_driver_resource_cache_oversized_storage_admission agc_driver_resource_cache_storage_clear agc_driver_resource_cache_replaced_texture agc_driver_resource_cache_lookup_changed PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() # The driver draw test again with the draw key off: the draw cache then keys on the decoded vertex @@ -2344,6 +2613,14 @@ if(BUILD_TESTING) add_test(NAME agc_descriptor_cache COMMAND agc_driver_graphics_tests --descriptor-cache) add_test(NAME agc_driver_address_space_teardown COMMAND agc_driver_graphics_tests --address-space-teardown) set_tests_properties(agc_driver_address_space_teardown PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_driver_pm4_rewind_trace COMMAND agc_driver_pm4_tests rewind-trace) + set_tests_properties(agc_driver_pm4_rewind_trace PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIPELINED_DRAWS=8;APS5_TRACE_FRAMES=0:1;APS5_TRACE_DIR=${CMAKE_CURRENT_BINARY_DIR}/pm4-rewind-trace" TIMEOUT 30) + set_property(TEST agc_driver_pm4_rewind_trace APPEND PROPERTY ENVIRONMENT_MODIFICATION + "APS5_TRACE_SECONDS=unset:" "APS5_TRACE_SUMMARY_EVERY=unset:") + if(WIN32) + set_property(TEST agc_driver_pm4_rewind_trace APPEND PROPERTY ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_driver_pm4_failure_inline COMMAND agc_driver_pm4_tests failure) set_tests_properties(agc_driver_pm4_failure_inline PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIPELINED_DRAWS=0" SKIP_RETURN_CODE 77) if(WIN32) @@ -2372,8 +2649,10 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_draw_cache_reuse_inline PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIPELINED_DRAWS=0;APS5_VERIFY_DRAW_RECIPE=1;APS5_VERIFY_REGISTER_KEYS=1" SKIP_RETURN_CODE 77) set_tests_properties(agc_driver_draw_cache_reuse agc_driver_draw_cache_reuse_off agc_driver_draw_cache_reuse_verify agc_driver_draw_cache_reuse_inline PROPERTIES FAIL_REGULAR_EXPRESSION "FATAL:") set_property(TEST agc_driver_draw_cache_reuse agc_driver_draw_cache_reuse_off APPEND PROPERTY ENVIRONMENT "APS5_VERIFY_DRAW_RECIPE=1" "APS5_VERIFY_REGISTER_KEYS=1") + add_test(NAME agc_driver_draw_data_hit_user_words_verify COMMAND agc_driver_draw_data_hit_user_words_tests) + set_tests_properties(agc_driver_draw_data_hit_user_words_verify PROPERTIES ENVIRONMENT "APS5_VERIFY_DRAW_DATA_HITS=1" SKIP_RETURN_CODE 77) if(WIN32) - set_tests_properties(agc_driver_draw_cache_reuse_off agc_driver_draw_cache_reuse_verify agc_driver_draw_cache_reuse_inline PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_driver_draw_cache_reuse_off agc_driver_draw_cache_reuse_verify agc_driver_draw_cache_reuse_inline agc_driver_draw_data_hit_user_words_verify PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() @@ -2389,6 +2668,7 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_pipelined_dispatch_item PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + set_tests_properties(agc_driver_queued_index_read_order PROPERTIES TIMEOUT 60) foreach(agcTest IN ITEMS agc_spirv_narrow_constant_store_tests agc_spirv13_image_tests agc_spirv_optimizer_tests agc_driver_constant_store_tests) if(NOT TARGET ${agcTest}) @@ -2528,6 +2808,7 @@ if(AGC_BUILD_VISUAL_TEST) target_link_libraries(agc_driver_visual_test PRIVATE shader_recompiler_agc_driver + decoder_png SDL2-static libc libkernel @@ -2611,8 +2892,25 @@ add_executable(agc_pm4_replay EXCLUDE_FROM_ALL target_include_directories(agc_pm4_replay PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(agc_pm4_replay PRIVATE libSceAgcDriver libc) set_target_properties(agc_pm4_replay PROPERTIES CXX_EXTENSIONS OFF) +if(MINGW) + target_link_options(agc_pm4_replay PRIVATE -municode) +endif() configure_windows_unwind(agc_pm4_replay) +if(MINGW) + get_filename_component(replayCompilerDirectory "${CMAKE_CXX_COMPILER}" DIRECTORY) + foreach(replayTool IN ITEMS agc_shader_replay agc_pm4_replay) + add_custom_command(TARGET ${replayTool} POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E copy_if_different + "${replayCompilerDirectory}/libgcc_s_seh-1.dll" + "${replayCompilerDirectory}/libstdc++-6.dll" + "${replayCompilerDirectory}/libwinpthread-1.dll" + "$" + VERBATIM + ) + endforeach() +endif() + if(BUILD_TESTING) add_test_executable(agc_driver_pm4_replay_tests tests/Pm4Replay.cpp tools/Pm4Analysis.cpp) target_include_directories(agc_driver_pm4_replay_tests PRIVATE ${LIBS_INCLUDE_DIR}) @@ -2658,10 +2956,13 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_covered_centroid_state PROPERTIES TIMEOUT 30) add_test(NAME agc_driver_sample_location_grid_fallback COMMAND agc_driver_graphics_tests --sample-location-grid-fallback) set_tests_properties(agc_driver_sample_location_grid_fallback PROPERTIES TIMEOUT 30) + add_test(NAME agc_driver_sample_order_planning COMMAND agc_driver_graphics_tests --sample-order) + set_tests_properties(agc_driver_sample_order_planning PROPERTIES TIMEOUT 30) if(WIN32) set_tests_properties(agc_compute_descriptor_layout_flags PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") set_tests_properties(agc_driver_covered_centroid_state PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") set_tests_properties(agc_driver_sample_location_grid_fallback PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_driver_sample_order_planning PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_compute_pipeline_capture COMMAND agc_compute_pipeline_capture_tests) set_tests_properties(agc_compute_pipeline_capture PROPERTIES TIMEOUT 30) @@ -3118,6 +3419,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/Driver/Diagnostics.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp index 3667d7bfd1..39bb6c35c9 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp @@ -3,6 +3,8 @@ #include "prx/libSceAgcDriver/Execution/include/QueueState.hpp" #include +#include +#include namespace AgcDriver::DriverDetail { @@ -10,6 +12,16 @@ double TraceMs(); std::uint64_t SkippedShader(); +class RefusedDraw final : public std::runtime_error { +public: + RefusedDraw(std::uint64_t address, const std::string& what) : std::runtime_error(what), address(address) {} + std::uint64_t address; +}; + +bool SkipRefusedDraws(); + +void ReportRefusedDraw(const RefusedDraw& refused); + void require(bool condition, const char* reason); std::uint32_t readRegister(const Registers& registers, std::uint32_t offset); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp index 75efb72f56..5f3df6c538 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp @@ -45,6 +45,7 @@ struct DispatchVariant { std::vector runBases; std::uint64_t relocatableWords = 0; std::vector> shaderDataPatches; + std::uint64_t userDataMask = ~std::uint64_t{0}; std::shared_ptr vertexInfo; std::shared_ptr memory; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp index ec149e3d6c..3e1f59e9f4 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include "prx/libc/include/general/AtomicSharedPtr.hpp" @@ -45,6 +46,9 @@ void ReadUserWords(const QueueState& queue, DrawProgram& program, bool staticAbi void InitializeMerged(const QueueState& queue, DrawProgram& program, std::uint32_t pointerBase, bool pointerRequired, bool staticAbi); void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const ShaderRegistry& registry, bool staticAbi, bool includeFragment); +void AppendProgramMemory(std::vector& memory, const std::vector& programs); +std::vector> UserDataWords(const std::vector& programs, const std::vector& roles, std::uint32_t location); + struct PreparedGraphicsStage { std::shared_ptr snapshot; PreparedShaders::Entry entry; @@ -122,6 +126,7 @@ struct StageCapture { std::uint64_t forgetSerial = 0; std::uint32_t pushOffset = 0; ShaderRecompiler::SrtUserDataUse userDataUse; + std::uint64_t userDataMask = ~std::uint64_t{0}; }; struct RelocatableUserWord { diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp index ef6a3e2e7d..9ca8b03dd5 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp @@ -32,7 +32,7 @@ public: static void FollowEpoch(std::uint64_t token); static unsigned Committers(); - void Enqueue(Commit commit, std::vector writes, std::uint64_t labelAddress = 0, std::vector labelBytes = {}); + void Enqueue(Commit commit, std::vector writes, std::uint64_t labelAddress = 0, std::vector labelBytes = {}, std::vector reads = {}); void Drain(DrainReason reason, std::uint32_t opcode = 0x100); void Stop(); static std::uint64_t DrainRequestsByOpcode(std::uint32_t opcode); @@ -40,6 +40,8 @@ public: std::size_t Outstanding() const { return outstanding.load(std::memory_order_acquire); } bool Overlaps(std::uint64_t address, std::size_t bytes); std::optional PendingLabel(std::uint64_t address, std::size_t bytes); + static bool HeldReads(std::uint64_t address, std::size_t bytes); + void ReleaseRunningReads(); private: struct Item { @@ -47,6 +49,7 @@ private: std::vector writes; std::uint64_t labelAddress = 0; std::vector labelBytes; + std::vector reads; }; DrawPipeline() = default; void run(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index 8b66e28ae6..3fb9f473b3 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -72,6 +72,7 @@ public: static void tagVariantRuns(DispatchVariant& variant, std::span regions, const ShaderRecompiler::SrtUserDataUse& use, bool eligible, std::uint32_t hiddenWords, std::span> supplied = {}); static std::string dumpRequest(std::uint64_t address, const ShaderRecompiler::RecompileRequest& request, std::string_view suffix = {}); DrawEntryCounters DrawCacheCounters(); + EntryCounters DispatchCacheCounters(); struct DispatchCacheFootprint { std::size_t entries = 0; std::uint64_t variantBytes = 0; @@ -163,7 +164,7 @@ private: std::shared_ptr decodeDraw(const QueueState& queue, const Submission& submission, const Graphics::Context& context); void resolveDrawDecode(const QueueState& queue, const Submission& submission, const Graphics::Context& context, std::shared_ptr& decode, bool registerKey, std::uint64_t drawKey, bool profile); void lookupDraw(const Submission& submission, const std::shared_ptr& localDevice, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector& programs, const std::vector& roles, const std::vector>& vertexInfos, bool useDrawEntries, bool registerKey, bool profile, std::uint64_t& drawKey, std::shared_ptr& entry, std::vector>& matched, std::vector>& matchedRegions, bool& drawHit, bool& verifyHit, DrawPhaseTiming& phaseTiming, std::array& phaseMs, const std::shared_ptr& dataEntry, DrawDataCandidates& dataCandidates, std::vector& stageData); - std::shared_ptr materializeDrawStage(std::size_t i, std::uint32_t pushOffset, const QueueState& queue, const Submission& submission, const std::vector& programs, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector>& vertexInfos, std::vector& memory, const std::vector& linked, const Pm4::DrawParameters& drawParameters, const std::shared_ptr& localDevice, ShaderMemory& shaderMemory, std::vector& stageCaptures, std::vector& recompiled, bool drawHit, const std::vector>& matched, const std::vector>& matchedRegions, bool profile, std::uint64_t dumpTarget, std::uint64_t dumpSlot1, std::uint64_t& captures, DrawPhaseTiming& phaseTiming, std::array& phaseMs, std::string& rejected); + std::shared_ptr materializeDrawStage(std::size_t i, std::uint32_t pushOffset, const QueueState& queue, const Submission& submission, const std::vector& programs, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector>& vertexInfos, std::vector& memory, const std::vector& linked, const Pm4::DrawParameters& drawParameters, const std::shared_ptr& localDevice, ShaderMemory& shaderMemory, std::vector& stageCaptures, std::vector& recompiled, bool drawHit, const std::vector>& matched, const std::vector>& matchedRegions, bool profile, std::uint64_t dumpTarget, std::uint64_t dumpSlot1, std::uint64_t& captures, DrawPhaseTiming& phaseTiming, std::array& phaseMs, std::string& rejected, const std::optional& meshBarycentrics); static void recordStageDataPositions(DispatchVariant& variant, const ShaderRecompiler::ResourceCapture& capture, const std::vector& decodeReads); void cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawParameters& drawParameters, const std::optional& indirectCpu, const std::vector& programs, const std::vector& stageCaptures, const std::vector>& vertexInfos, const std::vector>& decodeReads, bool verifyHit, const std::vector>& matched, std::vector>& fresh, std::uint64_t drawKey, bool registerKey, const std::shared_ptr& decode, DrawPhaseTiming& phaseTiming, const std::vector& reused, const std::shared_ptr& entry, std::uint64_t shapeKey); static bool drawPrecheck(); @@ -371,8 +372,7 @@ private: std::atomic dataPendingMisses{0}; - using ValueSet = std::pair>, std::vector>; - std::unordered_map> priorValueSets; + std::unordered_map> priorValueSets; std::unordered_map> drawCache; std::unordered_map drawShapes; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/DrawWriteRanges.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/DrawWriteRanges.hpp index 85cc30100c..261fd2f6f5 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/DrawWriteRanges.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/DrawWriteRanges.hpp @@ -1,6 +1,7 @@ #ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_DRAWWRITERANGES_HPP #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_DRAWWRITERANGES_HPP +#include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" #include "prx/libSceAgcDriver/Graphics/include/Shaders.hpp" #include "prx/libSceAgcDriver/Graphics/include/State.hpp" #include @@ -13,6 +14,7 @@ namespace AgcDriver::DriverDetail { std::vector> DrawWriteRanges(const Graphics::State& graphics, std::span stages, bool exact); std::vector> DispatchWriteRanges(const ShaderRecompiler::RecompileResult& compiled, bool& bounded); bool ExactDrawWrites(); +std::vector> QueuedDrawReadRanges(const Pm4::DrawParameters& draw, std::span stages); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/DwordPatternFill.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/DwordPatternFill.hpp index 9150a4583b..404207df81 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/DwordPatternFill.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/DwordPatternFill.hpp @@ -23,6 +23,7 @@ std::optional MatchDwordPatternFill(std::span MatchConstantDwordFill(std::span packet, std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute); std::optional MatchClampedGlobalFill(std::span packet, std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute); std::optional MatchMemoryValueBufferFill(std::span packet, std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute); +std::optional MatchSgprPatternFill(std::span packet, std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute); bool IsByteFill(const std::array& pattern); std::optional KeysFillMemo(const std::array& pattern, bool coversStorageKeys, bool memoryValueFill); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Synchronization/SynchronizationStatistics.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Synchronization/SynchronizationStatistics.hpp index 7e3244c66f..d807612d5d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Synchronization/SynchronizationStatistics.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Synchronization/SynchronizationStatistics.hpp @@ -30,7 +30,7 @@ enum TrySite { TryLabel = 0, TryFlush, TryReap, TryPoll, TryIdle, TryCapture, Tr extern std::atomic triesFailed[TrySites]; -extern std::atomic storesOnGpu, storesBehindCompletions, storesOnCpu, storesDrained, dmaFills, copiesToHostMemory; +extern std::atomic storesOnGpu, storesBehindCompletions, storesOnCpu, storesDrained, dmaFills, copiesToHostMemory, copiesBehindPendingSources; extern std::map drainCounts; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Float32FlushControl.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Float32FlushControl.hpp new file mode 100644 index 0000000000..0d93c2c2a4 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Float32FlushControl.hpp @@ -0,0 +1,20 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_FLOAT32FLUSHCONTROL_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_FLOAT32FLUSHCONTROL_HPP + +#include + +namespace AgcDriver { + +struct Float32FlushControl { + bool probe = false; + bool declared = false; +}; + +inline Float32FlushControl Float32FlushControlFor(VkDriverId driver, VkBool32 flushToZero, VkShaderFloatControlsIndependence independence) { + const bool declared = flushToZero == VK_TRUE && independence != VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_NONE; + return {declared || driver == VK_DRIVER_ID_MOLTENVK, declared}; +} + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp index 06c5c7bf53..efa0e9d031 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp @@ -64,6 +64,10 @@ void Unwatch(std::uint64_t address, std::size_t bytes); void Rewatch(std::span> ranges); bool ImportWatched(std::uint64_t address, std::size_t bytes, const std::function& import, bool staysWatched = true); void SetUnwatchHook(void (*hook)(std::uint64_t address, std::size_t bytes)); +#ifdef _WIN32 +using FreshCollectHook = bool (*)(std::uintptr_t address, std::size_t bytes, void (*written)(void*, std::uintptr_t, std::uintptr_t), void* context); +void SetFreshCollectHook(FreshCollectHook hook); +#endif bool Watched(std::uint64_t address, std::size_t bytes); std::uint64_t CollectWrites(std::uint64_t address, std::size_t bytes); bool UnchangedSince(std::uint64_t address, std::size_t bytes, std::uint64_t generation); 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/Pm4.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Pm4.hpp index f902632e1a..909b403e26 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Pm4.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Pm4.hpp @@ -89,7 +89,8 @@ void Validate(std::span packet, std::uint32_t queue); void Execute(std::span packet, QueueState& queue); inline bool IndirectRegisterOpcode(std::uint32_t opcode) { return opcode == 0x63 || opcode == 0x64 || opcode == 0x9f; } std::vector ReadIndirectRegisters(std::span packet); -void ExecuteIndirectRegisters(std::span packet, std::span pairs, QueueState& queue); +bool IndirectRegistersValid(std::span pairs); +void ExecuteIndirectRegisters(std::span packet, std::span pairs, QueueState& queue, bool skipInvalid = false); bool AccessesMemory(std::uint32_t header); // Whether an ACQUIRE_MEM packet asks only for GPU cache actions (no CPU-visible memory // synchronization): such a packet needs a pipeline barrier, not a device drain. 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/SubgroupBarrier.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/SubgroupBarrier.hpp new file mode 100644 index 0000000000..30f42ba2ab --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/SubgroupBarrier.hpp @@ -0,0 +1,14 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SUBGROUPBARRIER_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SUBGROUPBARRIER_HPP + +#include + +namespace AgcDriver { + +inline bool SubgroupControlBarrierUsable(VkDriverId driver) { + return driver != VK_DRIVER_ID_AMD_PROPRIETARY; +} + +} + +#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 4b8f170a2e..924a2701dd 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -11,6 +11,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libc/include/GuestAllocations.hpp" +#include #include #include #include @@ -45,6 +46,7 @@ public: // Starts the journal replay if the device has not (a headless device starts it at its first // draw or dispatch) and joins it (tests; the teardown joins it anyway). void WaitForPipelineJournalReplay(); + void SavePipelineCache(); // Compute pipelines this device built, by dispatches and by the journal replay (tests). std::uint64_t ComputePipelinesCreated() const; ~VulkanDevice(); @@ -54,6 +56,7 @@ public: std::string DriverInfo() const; bool CalibrateClock(double& gpuNs, std::chrono::steady_clock::time_point& cpu) const; ShaderRecompiler::SpirvTarget Target() const; + Graphics::SampleOrder HostSampleOrder(const Graphics::State& graphics) const; std::optional GeometryLimits() const; std::uint32_t MaxFragmentInputComponents() const; const std::uint32_t* WorkGroupCountLimit() const; @@ -103,6 +106,7 @@ public: // Pending-label table lookup and open-batch overlap test for WAIT_REG_MEM (see Recorder). std::optional PendingLabel(std::uint64_t address, std::size_t bytes, std::uint64_t afterStamp, Graphics::Recorder::LabelRefusal* refusal = nullptr) const; bool OpenWriteOverlaps(std::uint64_t address, std::size_t bytes) const; + void SealPassWriteOnly(); bool RecordedWritesSettled(std::uint64_t address, std::size_t bytes) const; bool StoresPendingOver(std::uint64_t address, std::size_t bytes) const; // Fills [address, address + bytes) of host-imported guest memory with a repeating 16-byte pattern, @@ -167,6 +171,7 @@ public: }; using CopyWriterNote = std::function value, std::uint64_t generation)>; CopyOutcome CopyBuffer(std::uint64_t destination, std::uint64_t source, std::size_t bytes, std::size_t cpuMax, std::size_t gpuMax, std::size_t knownMax, std::uint64_t programAddress, std::uint32_t queue, const CopyWriterNote& noteWriter); + bool GpuCopyPossible(std::uint64_t destination, std::uint64_t source, std::size_t bytes) const; // APS5_COPY_VERIFY=1 totals: CPU copies verified, whose source bytes changed over the sync (a // producer the rule missed), whose destination bytes were overwritten (a store the rule // missed), and whose destination an unsignaled batch read in place (the rule's own answer @@ -272,7 +277,7 @@ public: int cpuReason; double argumentReadMs; }; - IndirectOutcome DispatchIndirect(const ShaderRecompiler::RecompileResult& shader, std::uint64_t arguments, std::span snapshots = {}, std::uint64_t programAddress = 0, std::shared_ptr prepared = nullptr, std::shared_ptr* recipe = nullptr); + IndirectOutcome DispatchIndirect(const ShaderRecompiler::RecompileResult& shader, std::uint64_t arguments, std::span snapshots = {}, std::uint64_t programAddress = 0, std::shared_ptr prepared = nullptr, std::shared_ptr* recipe = nullptr, const std::array& threadGroup = {}); // A value-equal dispatch-cache hit with a recipe (design_cpu_final M4), in two steps. // PrepareRecipe, WITHOUT GuestMemory::GpuMutex: the pre-checks (recipes on, the recipe's // device is this one, its template and pipeline objects still alive), the template's @@ -294,7 +299,7 @@ public: // with the recipe's, else the process aborts. `refreshByWords`: the driver's data-only hit // (`shader` carries live flat-SRT words the recipe's hash does not name), so the template's // data refresh is decided by the per-word compare instead of the hash. - RecipeOutcome DispatchRecipe(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::uint64_t programAddress, const std::shared_ptr& hit, IndirectOutcome& outcome, const std::shared_ptr& verify = nullptr, bool refreshByWords = false); + RecipeOutcome DispatchRecipe(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::uint64_t programAddress, const std::shared_ptr& hit, IndirectOutcome& outcome, const std::shared_ptr& verify = nullptr, bool refreshByWords = false, const std::array& threadGroup = {}); // Whether dispatch-cache hits use recipes (APS5_NO_DISPATCH_RECIPE unset) and whether every // hit is verified (APS5_VERIFY_RECIPE=1). static bool DispatchRecipes(); @@ -314,6 +319,7 @@ public: // `recipe`, when given, receives the DrawRecipe a recorded, cacheable, reusable, direct draw // built for its draw-cache entry (design_cpu_final M8); null otherwise. void Draw(const Graphics::State& graphics, const Pm4::DrawParameters& draw, std::span shaders, std::span snapshots = {}, std::shared_ptr* recipe = nullptr); + bool DrawInputsInPlace() const; std::optional KnownDrawRejection(const Graphics::State& graphics, std::span shaders) const; void ColorMetadataPass(const Graphics::ColorMetadataPass& pass); // A draw-cache hit recorded from its recipe (Graphics::DrawWithRecipe), under the mutex after @@ -339,6 +345,8 @@ public: std::uint32_t computeSubgroupSize(const ShaderRecompiler::RecompileResult& shader) const; VkPipelineShaderStageCreateFlags computeStageFlags(const ShaderRecompiler::RecompileResult& shader) const; std::shared_ptr createComputePipeline(const Graphics::Context& context, const ShaderRecompiler::RecompileResult& shader, VkDescriptorSetLayout setLayout, std::span layoutKey, VkShaderStageFlags pushStages, std::uint64_t programAddress); + bool completionStoresOver(std::uint64_t address, std::size_t bytes) const; + bool copyWaitsOnCompletions(std::uint64_t address, std::size_t bytes) const; private: VulkanDevice(const PresentationWindow* window, bool compileOnly); @@ -347,7 +355,7 @@ private: ShaderRecompiler::SpirvTarget buildTarget() const; Graphics::Context buildContext() const; // Body of Dispatch and DispatchIndirect: `arguments` 0 dispatches x, y, z groups. - IndirectOutcome dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::span snapshots, std::uint64_t programAddress, std::shared_ptr prepared, std::shared_ptr* recipe); + IndirectOutcome dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::span snapshots, std::uint64_t programAddress, std::shared_ptr prepared, std::shared_ptr* recipe, const std::array& threadGroup = {}); // The stage-A pre-sync over `surfaces` (see PrepareDispatch): the serial waited for, 0 none. std::uint64_t presync(std::span surfaces); std::shared_ptr computePipeline(const Graphics::Context& context, const ShaderRecompiler::RecompileResult& shader, VkDescriptorSetLayout setLayout, std::span layoutKey, VkShaderStageFlags pushStages, std::uint64_t programAddress); @@ -358,6 +366,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/include/WorkerSampler.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp index ec282a171f..4aa7625ba9 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp @@ -5,6 +5,7 @@ namespace AgcDriver { // Starts sampling the calling thread when APS5_SAMPLE_WORKER names an output file. void StartWorkerSampler(); +void StartCommitterSampler(); void StopWorkerSampler(); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/WriteWatchCoverage.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/WriteWatchCoverage.hpp index 124d638998..22865df487 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/WriteWatchCoverage.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/WriteWatchCoverage.hpp @@ -23,7 +23,11 @@ public: if (bytes == 0 || bytes > size || address < base || address - base > size - bytes) return false; const auto first = (address - base) / BlockBytes; const auto last = (address - base + bytes - 1) / BlockBytes; - return std::none_of(excluded.begin() + first, excluded.begin() + last + 1, [](auto generation) { return generation != 0; }); + const auto firstPage = (address - base) % BlockBytes / PageBytes; + const auto lastPage = (address - base + bytes - 1) % BlockBytes / PageBytes; + if (first == last) return (excluded[first] & pageMask(firstPage, lastPage + 1)) == 0; + if ((excluded[first] & pageMask(firstPage, PagesPerBlock)) != 0 || (excluded[last] & pageMask(0, lastPage + 1)) != 0) return false; + return std::none_of(excluded.begin() + first + 1, excluded.begin() + last, [](auto mask) { return mask != 0; }); } bool ExcludedSpan(std::uint64_t address, std::size_t bytes, std::uint64_t* begin, std::uint64_t* end) const { @@ -40,10 +44,12 @@ public: if (!excludedBlocks(address, bytes, first, last)) return false; bool changed = false; for (auto block = first; block <= last; ++block) { - changed = changed || excluded[block] == 0; + const auto pages = (std::min(BlockBytes, size - block * BlockBytes) + PageBytes - 1) / PageBytes; + const auto mask = pageMask(0, pages); + changed = changed || excluded[block] != mask; auto& stamp = excludedGenerations[block]; stamp = std::max(stamp, std::max(generation, 1)); - excluded[block] = 1; + excluded[block] = mask; } return changed; } @@ -58,21 +64,28 @@ public: const auto offset = address - base; if (offset >= size) return false; bytes = static_cast(std::min(bytes, size - offset)); - const auto first = offset / BlockBytes + (offset % BlockBytes != 0); + const auto firstPage = offset / PageBytes + (offset % PageBytes != 0); const auto stop = offset + bytes; - const auto end = stop / BlockBytes + (stop == size && stop % BlockBytes != 0); + const auto endPage = stop / PageBytes + (stop == size && stop % PageBytes != 0); + if (firstPage >= endPage) return false; + const auto first = firstPage / PagesPerBlock; + const auto last = (endPage - 1) / PagesPerBlock; bool changed = false; - for (auto block = first; block < end; ++block) { - if (excluded[block] != 0 && excludedGenerations.at(block) < generation) { + for (auto block = first; block <= last; ++block) { + const auto mask = pageMask(block == first ? firstPage % PagesPerBlock : 0, block == last ? (endPage - 1) % PagesPerBlock + 1 : PagesPerBlock); + if ((excluded[block] & mask) != 0 && excludedGenerations.at(block) < generation) { changed = true; - excluded[block] = 0; - excludedGenerations.erase(block); + excluded[block] &= static_cast(~mask); + if (excluded[block] == 0) excludedGenerations.erase(block); } } return changed; } private: + static std::uint16_t pageMask(std::size_t first, std::size_t end) { + return static_cast(((1u << end) - 1u) & ~((1u << first) - 1u)); + } bool excludedBlocks(std::uint64_t address, std::size_t bytes, std::size_t& first, std::size_t& last) const { if (bytes == 0 || size == 0) return false; if (address < base) { @@ -88,9 +101,11 @@ private: return true; } static constexpr std::size_t BlockBytes = 65536; + static constexpr std::size_t PageBytes = 4096; + static constexpr std::size_t PagesPerBlock = BlockBytes / PageBytes; std::uint64_t base = 0; std::size_t size = 0; - std::vector excluded; + std::vector excluded; std::unordered_map excludedGenerations; }; 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/Diagnostics.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp index 3e80f40e9b..b94858e431 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp @@ -1,7 +1,11 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" +#include #include +#include +#include #include +#include namespace AgcDriver::DriverDetail { @@ -15,6 +19,23 @@ std::uint64_t SkippedShader() { return skipped; } +bool SkipRefusedDraws() { + static const bool skip = std::getenv("APS5_SKIP_REFUSED_DRAWS") != nullptr; + return skip; +} + +void ReportRefusedDraw(const RefusedDraw& refused) { + static std::mutex reportedMutex; + static std::set reported; + { + std::lock_guard lock(reportedMutex); + if (!reported.insert(refused.address).second) return; + } + const std::string_view what = refused.what(); + const auto line = what.substr(0, what.find('\n')); + std::fprintf(stderr, "[gpu] APS5_SKIP_REFUSED_DRAWS: skipping the draws that use shader 0x%llx: %.*s\n", static_cast(refused.address), static_cast(line.size()), line.data()); +} + void require(bool condition, const char* reason) { if (!condition) { throw std::runtime_error(std::string("AGC driver: ") + reason); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp index 0442fcd2d3..ccdc67b4aa 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp @@ -5,11 +5,14 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Memory/DrawWriteRanges.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Memory/BufferCopy.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Memory/DwordPatternFill.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/DeferredLabels.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "Optimization/ResourceProgram.hpp" #include #include @@ -117,9 +120,14 @@ void Driver::dispatch(QueueState& queue, std::span packet, const bool pipelineCandidate = !resolving && !fromAhead && DispatchItemsEnabled() && DrawPipeline::Active() && indirectArguments == 0 && deferredLabels().labels.empty() && !matchesCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute) && !MatchesStreamCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute); const bool partialLastGroup = partialGroups != std::array{}; if (resolving) { + if (indirectArguments == 0 && Graphics::MatchMultisampledSingleClear(std::span(snapshot.code).subspan(codeOffset), userData, compute.numThreads)) return; if (!partialLastGroup && matchesFillKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute)) return; if (!partialLastGroup && indirectArguments == 0 && (matchesCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute) || MatchesStreamCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute))) return; } else { + if (indirectArguments == 0 && Graphics::TakeMultisampledSingleClear(std::span(snapshot.code).subspan(codeOffset), userData, compute.numThreads)) { + pendingDispatchPhases().outcome = DispatchOutcome::FillHle; + return; + } if (indirectArguments == 0 && (packet[4] & 0x20u) == 0) { if (const auto fill = Graphics::MatchMemoryValueFill(std::span(snapshot.code).subspan(codeOffset), userData, compute.numThreads, {packet[1], packet[2], packet[3]})) Graphics::NoteMemoryValueFill(*fill); } @@ -145,11 +153,16 @@ void Driver::dispatch(QueueState& queue, std::span packet, } } if (partialLastGroup) compute.partialThreads = partialGroups; + std::array threadGroup{}; + if (indirectArguments != 0 && (packet[4] & 0x20u) != 0) { + for (std::uint32_t axis = 0; axis < 3; ++axis) threadGroup[axis] = std::max(readRegister(queue.shader, 0x207 + axis) & 0xffffu, 1u); + compute.partialThreads = {~0u, ~0u, ~0u}; + } ShaderRecompiler::RecompileRequest request{ {ShaderRecompiler::ShaderStage::Compute, address, std::span(snapshot.code).subspan(codeOffset), snapshot.headerAddress, snapshot.header}, {(packet[4] & 0x8000u) != 0 ? 32u : 64u, 0, userData, compute, std::nullopt, std::nullopt, memory, DispatchFloatMode(snapshot, queue.shader)}, localDevice->ComputeTarget((packet[4] & 0x8000u) != 0 ? 32u : 64u), - {0, 0, 0, 128} + {0, 0, 0, threadGroup != std::array{} ? 0u : 128u} }; static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; static double captureMs = 0, keyMs = 0, recompileMs = 0, deviceMs = 0; @@ -357,7 +370,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); @@ -382,8 +401,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; @@ -423,7 +442,7 @@ void Driver::dispatch(QueueState& queue, std::span packet, words += text; } } - std::fprintf(stderr, "[dispatch-io] shader 0x%llx%s\n", static_cast(address), words.c_str()); + std::fprintf(stderr, "[dispatch-io] queue=0x%x submission=%llu shader 0x%llx%s\n", submission.queue, static_cast(submission.serial), static_cast(address), words.c_str()); } const auto rethrow = [&](const std::exception& error) { @@ -443,7 +462,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; @@ -506,6 +525,8 @@ void Driver::dispatch(QueueState& queue, std::span packet, auto* const attachTo = stampValidate() ? nullptr : keepVariant != nullptr ? keepVariant.get() : attachVariant.get(); bool writersNoted = false; const bool noteWrites = writeEvidenceEnabled() || traceCapSync(); + const auto keysFill = indirectArguments == 0 && !partialLastGroup ? MatchSgprPatternFill(packet, std::span(snapshot.code).subspan(codeOffset), userData, compute) : std::nullopt; + const auto filledKeys = keysFill ? KeysFillMemo(keysFill->pattern, false, true) : std::nullopt; std::optional depthPlaneClear; if (indirectArguments == 0 && !partialLastGroup) { @@ -557,10 +578,12 @@ void Driver::dispatch(QueueState& queue, std::span packet, writersNoted = true; } phaseTiming.Phase(PhaseNoteWriters); + auto* const keysRecorder = filledKeys ? Graphics::Recorder::Active() : nullptr; + const auto keysNoteBefore = keysRecorder != nullptr ? keysRecorder->LastWriteNote(keysFill->base, keysFill->bytes) : 0; try { if (recipeHit != nullptr) { VulkanDevice::IndirectOutcome outcome{0, 0}; - const auto result = localDevice->DispatchRecipe(compiled, groups[0], groups[1], groups[2], indirectArguments, address, recipeHit, outcome, VulkanDevice::VerifyRecipes() ? prepared : nullptr, dataHit); + const auto result = localDevice->DispatchRecipe(compiled, groups[0], groups[1], groups[2], indirectArguments, address, recipeHit, outcome, VulkanDevice::VerifyRecipes() ? prepared : nullptr, dataHit, threadGroup); if (result == RecipeOutcome::Rebuild) { recipeHit = nullptr; @@ -569,7 +592,7 @@ void Driver::dispatch(QueueState& queue, std::span packet, } if (indirectArguments != 0) countIndirect(outcome.cpuReason, outcome.argumentReadMs); } else if (indirectArguments != 0) { - const auto outcome = localDevice->DispatchIndirect(compiled, indirectArguments, snapshots, address, std::move(prepared), attachTo != nullptr ? &builtRecipe : nullptr); + const auto outcome = localDevice->DispatchIndirect(compiled, indirectArguments, snapshots, address, std::move(prepared), attachTo != nullptr ? &builtRecipe : nullptr, threadGroup); countIndirect(outcome.cpuReason, outcome.argumentReadMs); } else { localDevice->Dispatch(compiled, groups[0], groups[1], groups[2], snapshots, address, std::move(prepared), attachTo != nullptr ? &builtRecipe : nullptr); @@ -577,6 +600,10 @@ void Driver::dispatch(QueueState& queue, std::span packet, } catch (const std::exception& error) { if (rethrow(error)) return; } + if (keysRecorder != nullptr && keysRecorder == Graphics::Recorder::Active()) { + const auto note = keysRecorder->LastWriteNote(keysFill->base, keysFill->bytes); + if (note != 0 && note != keysNoteBefore) Graphics::NoteKeysFillOnGpu(keysFill->base, keysFill->bytes, *filledKeys); + } if (builtRecipe != nullptr) { if (dataHit) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp index 97c5d143ae..732d7733b4 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp @@ -54,6 +54,11 @@ std::uint64_t Driver::DispatchCacheBytes() { return bytes; } +EntryCounters Driver::DispatchCacheCounters() { + std::lock_guard lock(dispatchCacheMutex); + return entryCounters; +} + Driver::DispatchCacheFootprint Driver::DispatchCacheSize() { std::lock_guard lock(dispatchCacheMutex); return {dispatchCache.size(), dispatchCacheVariantBytes}; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCacheStatistics.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCacheStatistics.cpp index dae236316c..5495a8c0aa 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCacheStatistics.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCacheStatistics.cpp @@ -7,9 +7,24 @@ namespace AgcDriver::DriverDetail { void Driver::classifyDiffering(std::uint64_t program, std::uint64_t key, const DispatchVariant& old, const DispatchVariant& fresh, const ShaderRecompiler::ResourceCapture* capture, EntryCounters& counters) { ++counters.differingClassified; ++counters.differingByProgram[program]; + const auto valueSet = [](const DispatchVariant& variant) { + std::uint64_t hash = 0xcbf29ce484222325ull; + const auto mix = [&](std::uint64_t value) { + hash ^= value; + hash *= 0x100000001b3ull; + }; + mix(variant.runs.size()); + for (const auto& [begin, end] : variant.runs) { + mix(begin); + mix(end); + } + mix(variant.words.size()); + for (const auto word : variant.words) mix(word); + return hash; + }; auto& ring = priorValueSets[key]; - if (std::any_of(ring.begin(), ring.end(), [&](const ValueSet& set) { return set.first == fresh.runs && set.second == fresh.words; })) ++counters.differingMatchedPrior; - ring.emplace_front(old.runs, old.words); + if (std::find(ring.begin(), ring.end(), valueSet(fresh)) != ring.end()) ++counters.differingMatchedPrior; + ring.push_front(valueSet(old)); while (ring.size() > 3) ring.pop_back(); if (priorValueSets.size() > dispatchCacheEntries()) priorValueSets.clear(); if (old.runs != fresh.runs || old.words.size() != fresh.words.size()) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/IndirectDispatch.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/IndirectDispatch.cpp index cd6ee2d743..65057babfa 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/IndirectDispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/IndirectDispatch.cpp @@ -48,7 +48,8 @@ void Driver::countIndirect(int path, double readMs) { int Driver::indirectArgumentsPath(std::span packet, std::uint64_t arguments) { static const bool gpuIndirect = std::getenv("APS5_NO_GPU_INDIRECT") == nullptr; if (!gpuIndirect) return IndirectDisabled; - if ((packet.back() & 0x20u) != 0) return IndirectThreadDimensions; + static const bool gpuThreadDimensions = std::getenv("APS5_NO_GPU_THREAD_DIMENSIONS") == nullptr; + if ((packet.back() & 0x20u) != 0 && !gpuThreadDimensions) return IndirectThreadDimensions; if (arguments % 4 != 0) return IndirectMisaligned; return IndirectGpu; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp index 7d11469870..dd0231145e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawProgramTrace.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Memory/DrawWriteRanges.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/DeferredLabels.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" @@ -36,7 +37,16 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe }; auto drawParameters = Pm4::ResolveDraw(packet, queue); const bool pipelined = DrawPipeline::Active(); - if (pipelined && drawParameters.indirect) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Indirect); + static const bool pipelinedIndirect = std::getenv("APS5_NO_PIPELINED_INDIRECT") == nullptr; + if (pipelined && drawParameters.indirect) { + const auto& indirect = *drawParameters.indirect; + auto& pipeline = DrawPipeline::Queue0(); + const bool overlaps = pipeline.Busy() && (pipeline.Overlaps(indirect.arguments, static_cast(std::max(indirect.RangeBytes(), indirect.recordBytes))) || (indirect.countIndirect && pipeline.Overlaps(indirect.countAddress, 4))); + if (!pipelinedIndirect || overlaps) pipeline.Drain(DrawPipeline::DrainReason::Indirect); + } + const auto drainBeforeInline = [&] { + if (pipelined && DrawPipeline::Queue0().Busy()) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Indirect); + }; bool traceIndirect = false; if (const auto verdict = precheckDraw(queue, submission, packet, drawParameters, rejected, traceIndirect)) return *verdict; phaseTiming.Phase(DrawRowPrecheck); @@ -206,13 +216,9 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe phaseTiming.Phase(DrawRowDecode); const auto locate = [&](std::uint32_t location) -> std::optional> { - if (location == 0x280u) return std::nullopt; - for (std::size_t i = 0; i < programs.size(); ++i) { - if (roles[i] == Role::Fragment || roles[i] == Role::GeometryBack || location < programs[i].userDataBase) continue; - const auto word = location - programs[i].userDataBase + (8u - programs[i].firstUserSgpr); - if (word < programs[i].userData.size()) return std::make_pair(i, static_cast(word)); - } - return std::nullopt; + const auto words = UserDataWords(programs, roles, location); + if (words.empty()) return std::nullopt; + return words.front(); }; if (drawParameters.indirect) { auto& indirect = *drawParameters.indirect; @@ -227,13 +233,11 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe } std::vector memory; memory.reserve(2 * programs.size() + 32); + AppendProgramMemory(memory, programs); std::vector linked; linked.reserve(programs.size()); for (std::size_t i = 0; i < programs.size(); ++i) { const auto& program = programs[i]; - for (const auto& region : program.memory) { - if (!region.bytes.empty()) memory.push_back(region); - } linked.push_back({roles[i], program.binary, program.userDataBase, program.firstUserSgpr, program.userData}); } timing.Mark("prepare"); @@ -370,6 +374,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe }; std::optional indirectCpu; std::vector pushOffsets(programs.size(), 0); + std::optional meshBarycentrics; std::vector resultIndex(programs.size(), 0); for (std::size_t i = 0; i < programs.size(); ++i) { if (roles[i] == Role::GeometryBack) continue; @@ -397,7 +402,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe break; } } - compiledStages.push_back(materializeDrawStage(i, pushCursorBytes, queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, drawParameters, localDevice, *shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected)); + compiledStages.push_back(materializeDrawStage(i, pushCursorBytes, queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, drawParameters, localDevice, *shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected, meshBarycentrics)); if (!rejected.empty()) return DrawVerdict::Rejected; programResults[i] = compiledStages.back().get(); if (candidate != dataCandidates[i].end()) verifyDataStage(i, *candidate->first); @@ -461,7 +466,21 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe phaseTiming.Phase(DrawRowRectList); }; if (graphics.rectList) buildRectList(); - if (!programs.empty() && programResults.back() != nullptr && programResults.back()->barycentricEmulation.active) { + if (!programs.empty() && programResults.back() != nullptr && programResults.back()->barycentricEmulation.active && graphics.stages.path == Graphics::ShaderPath::Geometry) { + require(localDevice->Target().mesh.has_value(), "a pixel shader on the mesh path reads barycentrics, and this device has neither VK_KHR_fragment_shader_barycentric nor mesh shaders (VK_EXT_mesh_shader), so they cannot be provided or emulated"); + require(!graphics.rectList && graphics.stages.mesh && programs.size() == 2 && programResults[0] != nullptr && programResults[1] != nullptr && stages.size() == 2, "a pixel shader that reads barycentrics needs exactly one mesh program before it to emulate them"); + meshBarycentrics = ShaderRecompiler::MeshBarycentricLayoutOf(*programResults[1]); + if (programResults[0]->meshBarycentrics != meshBarycentrics) { + const auto pushBytes = programResults[0]->pushConstants.size(); + const auto exports = programResults[0]->parameterExports; + compiledStages.push_back(materializeDrawStage(0, pushOffsets[0], queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, drawParameters, localDevice, *shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected, meshBarycentrics)); + if (!rejected.empty()) return DrawVerdict::Rejected; + programResults[0] = compiledStages.back().get(); + stages[0].program = programResults[0]; + require(programResults[0]->pushConstants.size() == pushBytes && programResults[0]->parameterExports == exports, "pairing the mesh program with its pixel shader changed the push constants or the parameter exports the pixel shader was built for"); + } + require(programResults[0]->meshBarycentrics == meshBarycentrics, "the mesh program was compiled for a different barycentric layout than its pixel shader"); + } else if (!programs.empty() && programResults.back() != nullptr && programResults.back()->barycentricEmulation.active) { require(!graphics.rectList && graphics.stages.path == Graphics::ShaderPath::Vertex && programs.size() == 2 && programResults[0] != nullptr && stages.size() == 2, "a pixel shader that reads barycentrics without VK_KHR_fragment_shader_barycentric needs a vertex shader before it"); const auto geometryLimits = localDevice->GeometryLimits(); if (geometryLimits || !localDevice->Target().tessellation) { @@ -469,7 +488,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe stages.insert(stages.begin() + 1, Graphics::CompiledShader{Stage::Geometry, &results.back(), 0}); } else { require(graphics.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST && !graphics.primitiveRestart, "a pixel shader that reads barycentrics without VK_KHR_fragment_shader_barycentric or geometry shaders needs a triangle list without primitive restart"); - auto patches = ShaderRecompiler::BuildBarycentricTessellationShaders(*programResults[0], *programResults[1], localDevice->Target(), localDevice->MaxFragmentInputComponents()); + auto patches = ShaderRecompiler::BuildBarycentricTessellationShaders(*programResults[0], *programResults[1], localDevice->Target(), localDevice->MaxFragmentInputComponents(), graphics.provokingVertexMode == VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT ? 2u : 0u); const auto first = results.size(); results.push_back(std::move(patches.control)); results.push_back(std::move(patches.evaluation)); @@ -519,22 +538,28 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe std::vector records; for (std::uint32_t record = 0; record < count; ++record) records.push_back(Pm4::ReadDrawArguments(indirect, record)); Graphics::CountIndirectDraw(*indirectCpu, std::chrono::duration(std::chrono::steady_clock::now() - readStart).count()); - const auto baseVertexWord = locate(indirect.baseVertexLocation); - const auto startInstanceWord = locate(indirect.startInstanceLocation); - const auto drawIndexWord = indirect.drawIndexEnabled ? locate(indirect.drawIndexLocation) : std::nullopt; + const auto baseVertexWords = UserDataWords(programs, roles, indirect.baseVertexLocation); + const auto startInstanceWords = UserDataWords(programs, roles, indirect.startInstanceLocation); + const auto drawIndexWords = indirect.drawIndexEnabled ? UserDataWords(programs, roles, indirect.drawIndexLocation) : std::vector>{}; + const auto drawable = [&](const Pm4::DrawArguments& arguments) { return arguments.count != 0 && arguments.instances != 0 && (!drawParameters.indexed || arguments.firstVertexOrIndex < drawParameters.indexCount); }; + std::size_t lastDrawable = records.size(); + for (std::size_t record = 0; record < records.size(); ++record) { + if (drawable(records[record])) lastDrawable = record; + } for (std::uint32_t record = 0; record < records.size(); ++record) { const auto& arguments = records[record]; if (traceIndirect) std::fprintf(stderr, "[draw] record %u: count %u instances %u first %u vertexOffset %u startInstance %u\n", record, arguments.count, arguments.instances, arguments.firstVertexOrIndex, arguments.vertexOffset, arguments.firstInstance); if (arguments.count == 0 || arguments.instances == 0) continue; std::set patched; - const auto patch = [&](const std::optional>& word, std::uint32_t value) { - if (!word) return; - writablePrograms[word->first].userData[word->second] = value; - patched.insert(word->first); + const auto patch = [&](const std::vector>& words, std::uint32_t value) { + for (const auto& [program, word] : words) { + writablePrograms[program].userData[word] = value; + patched.insert(program); + } }; - patch(baseVertexWord, indirect.recordBytes == 20 ? arguments.vertexOffset : arguments.firstVertexOrIndex); - patch(startInstanceWord, arguments.firstInstance); - patch(drawIndexWord, record); + patch(baseVertexWords, indirect.recordBytes == 20 ? arguments.vertexOffset : arguments.firstVertexOrIndex); + patch(startInstanceWords, arguments.firstInstance); + patch(drawIndexWords, record); if (graphics.stages.tessellation) { for (std::size_t i = 0; i < programs.size(); ++i) { if (programs[i].binary.stage == Stage::TessellationControl || programs[i].binary.stage == Stage::TessellationEvaluation) patched.insert(i); @@ -558,7 +583,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe auto& result = results[resultIndex[programIndex]]; const auto pushBytes = result.pushConstants.size(); decodeVertexInfo(programIndex); - result = *materializeDrawStage(programIndex, pushOffsets[programIndex], queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, programs[programIndex].binary.stage == Stage::TessellationControl || programs[programIndex].binary.stage == Stage::TessellationEvaluation ? direct : drawParameters, localDevice, *shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected); + result = *materializeDrawStage(programIndex, pushOffsets[programIndex], queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, programs[programIndex].binary.stage == Stage::TessellationControl || programs[programIndex].binary.stage == Stage::TessellationEvaluation ? direct : drawParameters, localDevice, *shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected, meshBarycentrics); if (!rejected.empty()) return DrawVerdict::Rejected; require(result.pushConstants.size() == pushBytes, "patched program changed its push constant layout"); } @@ -569,6 +594,24 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe rejected = std::move(*known); return DrawVerdict::Rejected; } + if (pipelined && pipelinedIndirect && record == lastDrawable && !gpuLock.owns_lock() && deferredLabels().labels.empty()) { + phaseTiming.Phase(DrawRowVectors); + auto writes = drawWriteRanges(graphics, stages); + const auto commitQueue = submission.queue; + const auto commitPacket = (packet[0] >> 8u) & 0xffu; + const auto epoch = DrawPipeline::EpochToken().load(std::memory_order_relaxed); + auto commit = [this, localDevice = std::move(localDevice), decode = std::move(decode), direct, stages = std::move(stages), snapshots = std::move(snapshots), commitQueue, commitPacket, epoch, programs = std::move(programs), results = std::move(results), memory = std::move(memory), linked = std::move(linked), stageCaptures = std::move(stageCaptures), matched = std::move(matched), matchedRegions = std::move(matchedRegions), fresh = std::move(fresh), decodeReads = std::move(decodeReads), shaderMemory = std::move(shaderMemory), entry = std::move(entry), dataEntry = std::move(dataEntry), compiledStages = std::move(compiledStages), stageData = std::move(stageData)] { + DrawPipeline::FollowEpoch(epoch); + GuestMemory::SetCurrentPacket(commitPacket, commitQueue); + commitDraw(localDevice, commitQueue, decode->state, direct, stages, snapshots, nullptr, {}, 0); + }; + auto held = std::make_shared(std::move(commit)); + DrawPipeline::Queue0().Enqueue([held] { (*held)(); }, std::move(writes)); + phaseTiming.Phase(DrawRowLockWait); + timing.Mark("draw_enqueued"); + return drawn(); + } + drainBeforeInline(); lockForDraw(); noteDrawWriters(stages, submission.queue); phaseTiming.Phase(DrawRowVectors); @@ -602,17 +645,19 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe const auto commitQueue = submission.queue; const auto commitPacket = (packet[0] >> 8u) & 0xffu; const auto epoch = DrawPipeline::EpochToken().load(std::memory_order_relaxed); + auto reads = QueuedDrawReadRanges(drawParameters, stages); auto commit = [this, localDevice = std::move(localDevice), decode = std::move(decode), drawParameters, stages = std::move(stages), snapshots = std::move(snapshots), recipe = std::move(recipe), recipeStages = std::move(recipeStages), drawKey, commitQueue, commitPacket, epoch, programs = std::move(programs), results = std::move(results), memory = std::move(memory), linked = std::move(linked), stageCaptures = std::move(stageCaptures), matched = std::move(matched), matchedRegions = std::move(matchedRegions), fresh = std::move(fresh), decodeReads = std::move(decodeReads), shaderMemory = std::move(shaderMemory), entry = std::move(entry), dataEntry = std::move(dataEntry), compiledStages = std::move(compiledStages), stageData = std::move(stageData)] { DrawPipeline::FollowEpoch(epoch); GuestMemory::SetCurrentPacket(commitPacket, commitQueue); commitDraw(localDevice, commitQueue, decode->state, drawParameters, stages, snapshots, recipe, recipeStages, drawKey); }; auto held = std::make_shared(std::move(commit)); - DrawPipeline::Queue0().Enqueue([held] { (*held)(); }, std::move(writes)); + DrawPipeline::Queue0().Enqueue([held] { (*held)(); }, std::move(writes), 0, {}, std::move(reads)); phaseTiming.Phase(DrawRowLockWait); timing.Mark("draw_enqueued"); return drawn(); } + if (drawParameters.indirect) drainBeforeInline(); lockForDraw(); noteDrawWriters(stages, submission.queue); phaseTiming.Phase(DrawRowVectors); 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 7662c966f0..c13d57f4a8 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp @@ -8,12 +8,13 @@ #include #include #include +#include #include #include namespace AgcDriver::DriverDetail { -std::shared_ptr Driver::materializeDrawStage(std::size_t i, std::uint32_t pushOffset, const QueueState& queue, const Submission& submission, const std::vector& programs, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector>& vertexInfos, std::vector& memory, const std::vector& linked, const Pm4::DrawParameters& drawParameters, const std::shared_ptr& localDevice, ShaderMemory& shaderMemory, std::vector& stageCaptures, std::vector& recompiled, bool drawHit, const std::vector>& matched, const std::vector>& matchedRegions, bool profile, std::uint64_t dumpTarget, std::uint64_t dumpSlot1, std::uint64_t& captures, DrawPhaseTiming& phaseTiming, std::array& phaseMs, std::string& rejected) { +std::shared_ptr Driver::materializeDrawStage(std::size_t i, std::uint32_t pushOffset, const QueueState& queue, const Submission& submission, const std::vector& programs, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector>& vertexInfos, std::vector& memory, const std::vector& linked, const Pm4::DrawParameters& drawParameters, const std::shared_ptr& localDevice, ShaderMemory& shaderMemory, std::vector& stageCaptures, std::vector& recompiled, bool drawHit, const std::vector>& matched, const std::vector>& matchedRegions, bool profile, std::uint64_t dumpTarget, std::uint64_t dumpSlot1, std::uint64_t& captures, DrawPhaseTiming& phaseTiming, std::array& phaseMs, std::string& rejected, const std::optional& meshBarycentrics) try { PerformanceTimer timing("Shader.DrawStage"); using Stage = ShaderRecompiler::ShaderStage; phaseTiming.Phase(DrawRowVectors); @@ -24,7 +25,7 @@ std::shared_ptr Driver::materializeDraw {waveSize, program.firstUserSgpr, program.userData, std::nullopt, program.binary.stage == Stage::Fragment ? std::optional(pixel) : std::nullopt, vertexInfos[i], memory, RegisteredFloatMode(*program.snapshot)}, localDevice->Target(), {0, 0, pushOffset, (graphics.stages.mesh ? ShaderRecompiler::MeshDrawPushOffsetBytes : graphics.stages.tessellation && pushOffset < Graphics::PipelinePushSlotBytes ? ShaderRecompiler::TessellationLevelPushOffsetBytes : graphics.depthRemap.mode != 0u ? ShaderRecompiler::DepthRemapPushOffsetBytes : Graphics::PipelinePushSlotBytes) - pushOffset % Graphics::PipelinePushSlotBytes}, - ShaderRecompiler::GraphicsCompileContext{program.firstUserSgpr, linked, graphics.stages.mesh, graphics.stages.tessellation, {drawParameters.indexAddress, drawParameters.indexCount, drawParameters.indexSize, drawParameters.instanceCount}} + ShaderRecompiler::GraphicsCompileContext{program.firstUserSgpr, linked, graphics.stages.mesh, graphics.stages.tessellation, {drawParameters.indexAddress, drawParameters.indexCount, drawParameters.indexSize, drawParameters.instanceCount}, program.binary.stage == Stage::Mesh ? meshBarycentrics : std::nullopt} }; const auto waitedBefore = traceCapSync() || profile ? Graphics::Recorder::ThreadWaitedMs() : 0.0; auto invocation = InvocationFor(*program.snapshot, program.codeOffset, request); @@ -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,12 +102,23 @@ 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"); + const auto walked = capture->userDataUse.asValue | capture->userDataUse.asBase; + auto mask = walked == 0 ? ~std::uint64_t{0} : walked | stageCapture.compiled->userDataRegisterReads; + if (vertexInfos[i]) { + for (const auto reg : {static_cast(vertexInfos[i]->fetchAttribReg), static_cast(vertexInfos[i]->fetchBufferReg)}) { + mask |= reg + 1u < 64u ? std::uint64_t{3} << reg : ~std::uint64_t{0}; + } + } + stageCapture.userDataMask = mask; phaseTiming.Phase(DrawRowRecompile); return stageCapture.compiled; +} catch (const std::exception& error) { + if (!SkipRefusedDraws()) throw; + throw RefusedDraw(programs[i].binary.codeAddress, error.what()); } void Driver::recordStageDataPositions(DispatchVariant& variant, const ShaderRecompiler::ResourceCapture& capture, const std::vector& decodeReads) { @@ -157,6 +171,7 @@ void Driver::cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawP variant->shader = programs[i].snapshot; variant->forgetSerial = stageCapture.forgetSerial; variant->pushOffset = stageCapture.pushOffset; + variant->userDataMask = stageCapture.userDataMask; if (vertexInfos[i]) variant->vertexInfo = std::make_shared(*vertexInfos[i]); std::vector regions(stageCapture.regions.begin(), stageCapture.regions.end()); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp index c9bc7f26bb..8208e0805d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp @@ -46,11 +46,34 @@ void InitializeMerged(const QueueState& queue, DrawProgram& program, std::uint32 const auto address = static_cast(low) | (static_cast(high) << 32u); require(address != 0 || !pointerRequired, "merged shader user-data address is null"); if (address == 0) return; - GuestMemory::CheckRange(reinterpret_cast(address), 8, 4); + if (pointerRequired) GuestMemory::CheckRange(reinterpret_cast(address), 8, 4); program.userData[0] = low; program.userData[1] = high; } +void AppendProgramMemory(std::vector& memory, const std::vector& programs) { + for (const auto& program : programs) { + for (const auto& region : program.memory) { + if (region.bytes.empty()) continue; + const bool listed = std::any_of(memory.begin(), memory.end(), [&](const ShaderRecompiler::MemoryRegion& known) { return known.guestAddress == region.guestAddress && known.bytes.data() == region.bytes.data() && known.bytes.size() == region.bytes.size(); }); + if (!listed) memory.push_back(region); + } + } +} + +std::vector> UserDataWords(const std::vector& programs, const std::vector& roles, std::uint32_t location) { + using Role = ShaderRecompiler::ProgramRole; + require(roles.size() == programs.size(), "draw program roles are incomplete"); + std::vector> words; + if (location == 0x280u) return words; + for (std::size_t i = 0; i < programs.size(); ++i) { + if (roles[i] == Role::Fragment || roles[i] == Role::GeometryBack || location < programs[i].userDataBase) continue; + const auto word = location - programs[i].userDataBase + (8u - programs[i].firstUserSgpr); + if (word < programs[i].userData.size()) words.emplace_back(i, static_cast(word)); + } + return words; +} + void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const ShaderRegistry& registry, bool staticAbi, bool includeFragment) { using Stage = ShaderRecompiler::ShaderStage; using Role = ShaderRecompiler::ProgramRole; @@ -95,11 +118,21 @@ void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const programs.push_back(prepare(programAddress(base), type, stage, resources, users)); roles.push_back(role); }; - const auto& graphics = product->state; + auto& graphics = product->state; if (graphics.stages.path == Graphics::ShaderPath::Tessellation) { - if (!staticAbi || programAddress(0x148) != 0) append(0x148, 5, Stage::Local, 0x10b, 0x10c, Role::Local); - append(0x108, 7, Stage::TessellationControl, 0x10b, 0x10c, Role::Hull); - InitializeMerged(queue, programs.back(), 0x102, true, staticAbi); + const auto localAddress = programAddress(0x148); + const auto local = !staticAbi || localAddress != 0 ? ProgramSnapshot(registry, localAddress, {3, 5}) : nullptr; + const bool mergedHull = local != nullptr && !local->header.empty() && local->type == 3; + require(graphics.stages.tessellation.has_value(), "tessellation draw has no tessellation configuration"); + graphics.stages.tessellation->mergedHull = mergedHull; + if (mergedHull) { + append(0x148, 3, Stage::Local, 0x10b, 0x10c, Role::Local); + append(0x148, 3, Stage::TessellationControl, 0x10b, 0x10c, Role::Hull); + } else { + if (local != nullptr) append(0x148, 5, Stage::Local, 0x10b, 0x10c, Role::Local); + append(0x108, 7, Stage::TessellationControl, 0x10b, 0x10c, Role::Hull); + } + InitializeMerged(queue, programs.back(), 0x102, !mergedHull, staticAbi); append(0x0c8, 2, Stage::TessellationEvaluation, 0x08b, 0x08c, Role::Domain); } else if (graphics.stages.path == Graphics::ShaderPath::Geometry) { const auto frontAddress = programAddress(0xc8); @@ -134,7 +167,11 @@ std::shared_ptr Driver::decodeDraw(const QueueState& queue, const Su product->pixel.reversedBlend = product->state.reversedBlend; product->pixel.alphaToCoverage = product->state.alphaToCoverage; product->pixel.quadPixelMask = Graphics::QuadPixelMask(product->state); - product->pixel.quadSampleMask = Graphics::QuadSampleMasks(product->state); + if (product->state.sampleLocationGrid) { + const auto localDevice = device.Load(); + if (localDevice != nullptr) product->pixel.sampleOrder = localDevice->HostSampleOrder(product->state); + } + product->pixel.quadSampleMask = Graphics::QuadSampleMasks(product->state, product->pixel.sampleOrder); if (product->pixel.stencilRefExportEnable && Graphics::StencilReferenceUnused(product->state)) { const auto localDevice = device.Load(); const auto capabilities = localDevice == nullptr ? std::vector{} : localDevice->Target().supportedCapabilities; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp index 8c7e397fa8..6bfa9b1fb4 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp @@ -28,6 +28,7 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr(tess.breakAtInstance)}) mix(value); + mix(tess.mergedHull); } mix(graphics.rectList); mix(programs.size()); @@ -67,6 +68,7 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr(pixel.conservativeZExport)); mix(pixel.orderedPixelShader); mix(pixel.stencilRefExportEnable); + mix(pixel.stencilOperationValueExport); for (const auto value : pixel.targetOutputMode) mix(value); for (const auto value : pixel.targetExportMapping) mix(value); for (const auto value : pixel.targetExportPacking) mix(static_cast(value)); @@ -80,6 +82,7 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr(pixel.positionLocation)); for (const auto location : pixel.sampleLocations) mix(location); + for (const auto order : pixel.sampleOrder) mix(order); std::lock_guard cacheLock(drawCacheMutex); ++drawEntryCounters.lookups; const auto found = drawCache.find(drawKey); @@ -213,9 +216,16 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr sampling; for (std::size_t i = 0; i < programs.size(); ++i) { - if (roles[i] == Role::GeometryBack || programs[i].userData != dataEntry->decode->programs[i].userData) continue; + if (roles[i] == Role::GeometryBack || programs[i].userData.size() != dataEntry->decode->programs[i].userData.size()) continue; + const auto& now = programs[i].userData; + const auto& was = dataEntry->decode->programs[i].userData; auto& candidates = dataCandidates[i]; for (const auto& variant : dataEntry->stages[i]) { + bool usedEqual = true; + for (std::size_t w = 0; w < now.size() && usedEqual; ++w) { + if (now[w] != was[w] && (w >= 64u || ((variant->userDataMask >> w) & 1u) != 0)) usedEqual = false; + } + if (!usedEqual) continue; if ((i == 0 && variant->pushOffset != 0) || std::any_of(candidates.begin(), candidates.end(), [&](const auto& candidate) { return candidate.first->pushOffset == variant->pushOffset; })) continue; std::vector regions; appendEntryRegions(*variant, regions); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp index 420ddb33a4..9ba7b4a1db 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp @@ -3,6 +3,7 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/HandoffSpin.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp" +#include "prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp" #include #include #include @@ -22,6 +23,10 @@ std::uint64_t elapsedNs(std::chrono::steady_clock::time_point since) { return static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - since).count()); } +std::atomic heldReadRanges{0}; +std::atomic heldReadQueries{0}; +std::atomic heldReadHits{0}; + } DrawPipeline& DrawPipeline::Queue0() { @@ -73,7 +78,7 @@ void DrawPipeline::rethrowFailure() { std::rethrow_exception(error); } -void DrawPipeline::Enqueue(Commit commit, std::vector writes, std::uint64_t labelAddress, std::vector labelBytes) { +void DrawPipeline::Enqueue(Commit commit, std::vector writes, std::uint64_t labelAddress, std::vector labelBytes, std::vector reads) { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; std::unique_lock lock(mutex); rethrowFailure(); @@ -93,7 +98,8 @@ void DrawPipeline::Enqueue(Commit commit, std::vector writes, std::uint64 } depthSum += items.size(); ++enqueued; - items.push_back({std::move(commit), std::move(writes), labelAddress, std::move(labelBytes)}); + heldReadRanges.fetch_add(reads.size(), std::memory_order_relaxed); + items.push_back({std::move(commit), std::move(writes), labelAddress, std::move(labelBytes), std::move(reads)}); outstanding.fetch_add(1, std::memory_order_release); if (!thread.joinable()) { thread = std::thread([this] { run(); }); @@ -126,7 +132,10 @@ void DrawPipeline::report(std::chrono::steady_clock::time_point now) { const auto perCommit = commits == 0 ? 0.0 : static_cast(commitNs) / 1e3 / static_cast(commits); const auto perDraw = enqueued == 0 ? 0.0 : static_cast(fullWaitNs) / 1e3 / static_cast(enqueued); const auto queued = enqueued == 0 ? 0.0 : static_cast(depthSum) / static_cast(enqueued); - std::fprintf(stderr, "[draw] pipelined (10 s, depth %zu): %llu commits at %.1f us each on the committer, %llu failed; %llu enqueued behind %.2f on average, waiting %.1f us per draw for room; drains:%s\n", Depth(), static_cast(commits), perCommit, static_cast(commitErrors), static_cast(enqueued), queued, perDraw, drainText.c_str()); + char reads[64]; + std::snprintf(reads, sizeof(reads), " %llu read ranges, %llu stores held", static_cast(heldReadRanges.load(std::memory_order_relaxed)), static_cast(heldReadHits.exchange(0, std::memory_order_relaxed))); + drainText += reads; + std::fprintf(stderr, "[draw] pipelined (10 s, depth %zu): %llu commits at %.1f us each on the committer, %llu failed; %llu enqueued behind %.2f on average, waiting %.1f us per draw for room;%s; drains:%s\n", Depth(), static_cast(commits), perCommit, static_cast(commitErrors), static_cast(enqueued), queued, perDraw, reads, drainText.c_str()); commits = commitNs = commitErrors = enqueued = fullWaitNs = depthSum = 0; drains = {}; drainWaitNs = {}; @@ -134,6 +143,7 @@ void DrawPipeline::report(std::chrono::steady_clock::time_point now) { void DrawPipeline::run() { PerformanceTrace::BindThread("draw committer"); + StartCommitterSampler(); for (;;) { Commit* commit = nullptr; SpinUntil(HandoffSpinWindow(), [&] { return outstanding.load(std::memory_order_acquire) != 0; }); @@ -211,6 +221,27 @@ std::uint64_t DrawPipeline::DrainRequestsByOpcode(std::uint32_t opcode) { return Queue0().drainRequests[std::min(opcode, 0x100)].load(std::memory_order_relaxed); } +bool DrawPipeline::HeldReads(std::uint64_t address, std::size_t bytes) { + if (bytes == 0 || std::getenv("APS5_NO_QUEUED_DRAW_READ_ORDER") != nullptr) return false; + const auto end = address + bytes; + auto& pipeline = Queue0(); + std::lock_guard lock(pipeline.mutex); + heldReadQueries.fetch_add(1, std::memory_order_relaxed); + for (const auto& item : pipeline.items) { + for (const auto& [begin, limit] : item.reads) { + if (address >= limit || begin >= end) continue; + heldReadHits.fetch_add(1, std::memory_order_relaxed); + return true; + } + } + return false; +} + +void DrawPipeline::ReleaseRunningReads() { + std::lock_guard lock(mutex); + if (!items.empty()) items.front().reads.clear(); +} + std::optional DrawPipeline::PendingLabel(std::uint64_t address, std::size_t bytes) { if (bytes == 0 || bytes > 8) return std::nullopt; const auto end = address + bytes; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp index b27cadf179..dd66e54ebf 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp @@ -241,7 +241,7 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) { const auto sameTess = [](const std::optional& x, const std::optional& y) { if (x.has_value() != y.has_value()) return false; if (!x) return true; - return x->inputControlPoints == y->inputControlPoints && x->outputControlPoints == y->outputControlPoints && x->domain == y->domain && x->partitioning == y->partitioning && x->outputTopology == y->outputTopology && x->patchesPerGroup == y->patchesPerGroup && x->primitiveGroupSize == y->primitiveGroupSize && x->breakAtInstance == y->breakAtInstance; + return x->inputControlPoints == y->inputControlPoints && x->outputControlPoints == y->outputControlPoints && x->domain == y->domain && x->partitioning == y->partitioning && x->outputTopology == y->outputTopology && x->patchesPerGroup == y->patchesPerGroup && x->primitiveGroupSize == y->primitiveGroupSize && x->breakAtInstance == y->breakAtInstance && x->mergedHull == y->mergedHull; }; if (s.stages.path != t.stages.path || s.stages.registerValue != t.stages.registerValue || s.stages.vertexWaveSize != t.stages.vertexWaveSize || s.stages.fragmentWaveSize != t.stages.fragmentWaveSize || !sameMesh(s.stages.mesh, t.stages.mesh) || !sameTess(s.stages.tessellation, t.stages.tessellation)) return false; if (!sameColor(s.color, t.color) || s.colors.size() != t.colors.size() || s.blends.size() != t.blends.size()) return false; @@ -251,18 +251,18 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) { for (std::size_t i = 0; i < s.blends.size(); ++i) { if (!sameBlend(s.blends[i], t.blends[i])) return false; } - if (s.hasColorTarget != t.hasColorTarget || s.rectList != t.rectList || s.renderExtent.width != t.renderExtent.width || s.renderExtent.height != t.renderExtent.height || s.topology != t.topology || s.negativeOneToOne != t.negativeOneToOne || s.depthClamp != t.depthClamp || s.conservativeRasterization != t.conservativeRasterization || s.perSampleShading != t.perSampleShading || s.cullMode != t.cullMode || s.frontFace != t.frontFace || !sameBlend(s.blend, t.blend) || s.blendConstants != t.blendConstants) return false; + if (s.hasColorTarget != t.hasColorTarget || s.rectList != t.rectList || s.renderExtent.width != t.renderExtent.width || s.renderExtent.height != t.renderExtent.height || s.topology != t.topology || s.negativeOneToOne != t.negativeOneToOne || s.depthClamp != t.depthClamp || s.conservativeRasterization != t.conservativeRasterization || s.perSampleShading != t.perSampleShading || s.cullMode != t.cullMode || s.polygonMode != t.polygonMode || s.frontFace != t.frontFace || !sameBlend(s.blend, t.blend) || s.blendConstants != t.blendConstants) return false; if (std::memcmp(&s.viewport, &t.viewport, sizeof(VkViewport)) != 0 || std::memcmp(&s.scissor, &t.scissor, sizeof(VkRect2D)) != 0) return false; const auto& p = a.pixel; const auto& q = b.pixel; - if (p.interpolatorCount != q.interpolatorCount || p.interpolatorSettings != q.interpolatorSettings || p.wave32 != q.wave32 || p.inputAddr != q.inputAddr || p.hasPerspectiveCenterVgpr != q.hasPerspectiveCenterVgpr || p.perspectiveCentroid != q.perspectiveCentroid || p.posX != q.posX || p.posY != q.posY || p.posZ != q.posZ || p.posW != q.posW || p.frontFace != q.frontFace || p.frontFaceAllBits != q.frontFaceAllBits || p.ancillary != q.ancillary || p.sampleShading != q.sampleShading || p.noPerspective != q.noPerspective || p.linearCentroid != q.linearCentroid || p.pixelKillEnable != q.pixelKillEnable || p.depthExportEnable != q.depthExportEnable || p.sampleMaskExportEnable != q.sampleMaskExportEnable || p.earlyZ != q.earlyZ || p.executeOnNoop != q.executeOnNoop || p.conservativeZExport != q.conservativeZExport || p.orderedPixelShader != q.orderedPixelShader || p.stencilRefExportEnable != q.stencilRefExportEnable || p.targetOutputMode != q.targetOutputMode || p.targetExportMapping != q.targetExportMapping) return false; + if (p.interpolatorCount != q.interpolatorCount || p.interpolatorSettings != q.interpolatorSettings || p.wave32 != q.wave32 || p.inputAddr != q.inputAddr || p.hasPerspectiveCenterVgpr != q.hasPerspectiveCenterVgpr || p.perspectiveCentroid != q.perspectiveCentroid || p.posX != q.posX || p.posY != q.posY || p.posZ != q.posZ || p.posW != q.posW || p.frontFace != q.frontFace || p.frontFaceAllBits != q.frontFaceAllBits || p.ancillary != q.ancillary || p.sampleShading != q.sampleShading || p.noPerspective != q.noPerspective || p.linearCentroid != q.linearCentroid || p.pixelKillEnable != q.pixelKillEnable || p.depthExportEnable != q.depthExportEnable || p.sampleMaskExportEnable != q.sampleMaskExportEnable || p.earlyZ != q.earlyZ || p.executeOnNoop != q.executeOnNoop || p.conservativeZExport != q.conservativeZExport || p.orderedPixelShader != q.orderedPixelShader || p.stencilRefExportEnable != q.stencilRefExportEnable || p.stencilOperationValueExport != q.stencilOperationValueExport || p.targetOutputMode != q.targetOutputMode || p.targetExportMapping != q.targetExportMapping) return false; if (a.roles != b.roles || a.programs.size() != b.programs.size()) return false; for (std::size_t i = 0; i < a.programs.size(); ++i) { const auto& x = a.programs[i]; const auto& y = b.programs[i]; if (x.binary.stage != y.binary.stage || x.binary.codeAddress != y.binary.codeAddress || x.userDataBase != y.userDataBase || x.firstUserSgpr != y.firstUserSgpr || x.userData != y.userData || x.snapshot != y.snapshot || x.codeOffset != y.codeOffset || x.tailContinuation != y.tailContinuation) return false; } - if (p.targetExportPacking != q.targetExportPacking || p.dualSourceBlend != q.dualSourceBlend || p.reversedBlend != q.reversedBlend || p.alphaToCoverage != q.alphaToCoverage || p.rasterPrimitive != q.rasterPrimitive || p.ditheredAlphaToCoverage != q.ditheredAlphaToCoverage || p.quadPixelMask != q.quadPixelMask || p.quadSampleMask != q.quadSampleMask || p.iteratesSamples != q.iteratesSamples || p.positionLocation != q.positionLocation || p.sampleLocations != q.sampleLocations) return false; + if (p.targetExportPacking != q.targetExportPacking || p.dualSourceBlend != q.dualSourceBlend || p.reversedBlend != q.reversedBlend || p.alphaToCoverage != q.alphaToCoverage || p.rasterPrimitive != q.rasterPrimitive || p.ditheredAlphaToCoverage != q.ditheredAlphaToCoverage || p.quadPixelMask != q.quadPixelMask || p.quadSampleMask != q.quadSampleMask || p.iteratesSamples != q.iteratesSamples || p.positionLocation != q.positionLocation || p.sampleLocations != q.sampleLocations || p.sampleOrder != q.sampleOrder) return false; if (s.samples != t.samples || s.mergedCoverage != t.mergedCoverage || s.quadSampleMask != t.quadSampleMask || s.pixelEarlyZ != t.pixelEarlyZ) return false; return true; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp index bf6d16754a..04a1f72a02 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp @@ -1,10 +1,13 @@ #include "prx/libSceAgcDriver/Execution/include/ThreadPriority.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Screenshot.hpp" +#include "prx/libkernel/Time/include/SleepWake.hpp" #include namespace AgcDriver { void Submit(const Packet* packet, std::uint32_t queue) try { + KernelNoteGpuSubmit_nid_no_patch(); DriverDetail::Driver::Get().Submit(packet, queue); } catch (const ProcessShutdown&) { LibcAwaitExit_nid_postfix(); @@ -83,6 +86,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 23c3b254d3..71c479e41e 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; @@ -166,6 +166,7 @@ void Driver::stop() { CloseRegistrationPreparation(); Graphics::StopPipelineLibraryWorker(); std::lock_guard gpuLock(GuestMemory::GpuMutex()); + Graphics::ReportHostImports(); device.Reset(); replacedDevices.clear(); Graphics::FlushPipelineJournal(); @@ -182,6 +183,10 @@ Driver::Driver() { InstallAsyncLog_nid_no_patch(); static const bool driverMemory = (MapDriverMemory(), true); static_cast(driverMemory); + LibcSetExitDrain_nid_no_patch([] { + const auto current = Driver::Get().device.Load(); + if (current) current->SavePipelineCache(); + }); static_cast(GuestMemory::TrackerGeneration()); ShaderMemory::SetWaitedMsProvider(&Graphics::Recorder::ThreadWaitedMs); try { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp index 603609290c..e410cb88d3 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp @@ -151,6 +151,26 @@ std::optional KeysFillMemo(const std::array } } +std::optional MatchSgprPatternFill(std::span packet, std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute) { + if (userData.size() != 9 || packet.size() < 5 || !compute.groupIdEnable[0] || compute.numThreads[0] != 64 || compute.numThreads[1] != 1 || compute.numThreads[2] != 1) return std::nullopt; + static constexpr std::array kernel{ + 0xbfa00001u, 0xd7460004u, 0x04010c09u, 0x7e000204u, 0x7e020205u, 0x7e040206u, 0x7e060207u, 0x7da80808u, + 0xbf88000du, 0x360a0883u, 0xbe84047eu, 0xbe86047eu, 0x7e100505u, 0x7da40a08u, 0xbefc0308u, 0x7e0c8700u, + 0x8a867e06u, 0xbefe0406u, 0xbf85fff9u, 0xbefe0404u, 0xe0102000u, 0x80000604u, 0xbf810000u}; + if (code.size() < kernel.size() || !std::equal(kernel.begin(), kernel.end(), code.begin())) return std::nullopt; + const auto stride = (userData[1] >> 16u) & 0x3fffu; + const bool swizzled = ((userData[1] >> 31u) & 1u) != 0; + const bool addTid = ((userData[3] >> 23u) & 1u) != 0; + const auto outOfBounds = (userData[3] >> 28u) & 3u; + const auto format = (userData[3] >> 12u) & 0x7fu; + const auto type = userData[3] >> 30u; + if (type != 0 || stride != 4 || swizzled || addTid || outOfBounds > 1u || format < 20u || format > 22u || (packet[4] & 0x20u) != 0 || packet[2] != 1 || packet[3] != 1) return std::nullopt; + const auto dwords = std::min({static_cast(packet[1]) * 64u, userData[8], userData[2]}); + if (dwords == 0) return std::nullopt; + const auto base = userData[0] | (static_cast(userData[1] & 0xffffu) << 32u); + return DwordPatternFill{base, static_cast(dwords * 4u), {userData[4], userData[5], userData[6], userData[7]}}; +} + bool Driver::fillClearEnabled() { static const bool enabled = std::getenv("APS5_NO_FILL_CLEAR") == nullptr; return enabled; 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 47b88455e4..8b0687ac4f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WrittenBuffers.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WrittenBuffers.cpp @@ -4,10 +4,13 @@ #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp" #include "Optimization/ShaderStageInputInfo.hpp" #include #include #include +#include +#include namespace AgcDriver::DriverDetail { @@ -64,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)); @@ -156,6 +160,40 @@ std::vector> Driver::drawWriteRanges(con return DrawWriteRanges(graphics, stages, ExactDrawWrites()); } +std::vector> QueuedDrawReadRanges(const Pm4::DrawParameters& draw, std::span stages) { + std::vector> ranges; + const auto add = [&](std::uint64_t begin, std::uint64_t bytes) { + if (begin == 0 || bytes == 0 || bytes > std::numeric_limits::max() - begin) return; + ranges.emplace_back(begin, begin + bytes); + }; + if (draw.indexed && draw.indexSize != 0 && draw.indexCount != 0) add(draw.indexAddress, static_cast(draw.indexCount) * draw.indexSize); + if (stages.empty()) return ranges; + for (const auto& stage : stages) { + if (stage.program == nullptr) continue; + for (const auto& binding : stage.program->bindings) { + if (binding.role != ShaderRecompiler::DescriptorRole::GuestBuffers || binding.guestDescriptor.size() < static_cast(binding.count) * 4u) continue; + for (std::uint32_t element = 0; element < binding.count; ++element) { + const auto* words = binding.guestDescriptor.data() + static_cast(element) * 4u; + const ShaderRecompiler::ShaderBufferResource descriptor{{words[0], words[1], words[2], words[3]}}; + add(descriptor.Base48(), descriptor.GetSize()); + } + } + } + if (stages.empty() || stages.front().program == nullptr) return ranges; + for (const auto& attribute : stages.front().program->vertexAttributes) { + if (Graphics::NullVertexDescriptor(attribute) || Graphics::VertexFetchOutOfRange(attribute)) continue; + const auto address = attribute.resource.fields[0] | (static_cast(attribute.resource.fields[1] & 0xffffu) << 32u); + std::uint64_t bytes = 0; + try { + bytes = Graphics::VertexBufferExtent(attribute); + } catch (const std::runtime_error&) { + continue; + } + add(address, bytes); + } + return ranges; +} + std::optional Driver::newestWriterLocked(std::uint64_t begin, std::uint64_t end) const { for (auto it = writtenBuffers.rbegin(); it != writtenBuffers.rend(); ++it) { if (begin < it->end && it->begin < end) return *it; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp index 077fef550a..ce9a656cb7 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp @@ -11,9 +11,12 @@ #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" #include "prx/libSceAgcDriver/Eq/include/Event.hpp" +#include +#include #include #include #include +#include namespace AgcDriver::DriverDetail { @@ -53,6 +56,14 @@ bool keepsDrawPipeline(std::span packet, std::uint32_t head const auto address = packet[4] | (static_cast(packet[5]) << 32u); return !DrawPipeline::Queue0().Overlaps(address, bytes); } + case 0x3c: case 0x93: { + if (packet.size() < 5) return false; + const std::uint64_t awaited = packet[2] | (static_cast(packet[3]) << 32u); + const auto bytes = Pm4::WaitAwaitedBytes(packet); + if (DrawPipeline::Queue0().Overlaps(awaited, bytes)) return false; + GuestMemory::CheckRange(reinterpret_cast(awaited), bytes, bytes); + return Pm4::WaitSatisfiedUnchecked(packet); + } case 0x10: { const auto operation = (header >> 2u) & 0x3fu; return operation == 0 || operation == 0x09 || operation == 0x0b || operation == 0x0c || operation == 0x1a; @@ -61,6 +72,20 @@ bool keepsDrawPipeline(std::span packet, std::uint32_t head } } +bool sealsWriteOnly(std::span packet, std::uint32_t opcode) { + if (opcode == 0x58) return true; + if (opcode != 0x46 || packet.size() < 2) return false; + switch (packet[1] & 0x3fu) { + case 0x07: case 0x0f: case 0x10: case 0x16: case 0x2a: case 0x2c: case 0x2e: case 0x31: return true; + default: return false; + } +} + +bool& drawnSinceSeal() { + static thread_local bool drawn = false; + return drawn; +} + } void Driver::deliverGuestEopInterrupt(std::uint32_t queue) { @@ -350,14 +375,15 @@ void Driver::execute(const Submission& submission) { timing.Mark("flip_flush"); timing.FinishPacket(submission.serial, cursor, 0xffffu, count); - auto frame = APS5_ENABLE_TIMING_LOG ? std::move(pendingFrameTiming) : std::make_shared(++frameSerial); + const bool queueTiming = APS5_ENABLE_TIMING_LOG && submission.queue == 0; + auto frame = queueTiming ? std::move(pendingFrameTiming) : std::make_shared(++frameSerial); require(frame != nullptr, "flip has no frame timing"); const auto now = FrameTiming::Clock::now(); - if (!APS5_ENABLE_TIMING_LOG) frame->IncludeSubmission(submission.serial, now, now, now, true); - frame->SetFlip(submission.serial, cursor, APS5_ENABLE_TIMING_LOG ? submission.receivedAt : now, now); - if (APS5_ENABLE_TIMING_LOG) frame->CollectBackground(); + if (!queueTiming) frame->IncludeSubmission(submission.serial, now, now, now, true); + frame->SetFlip(submission.serial, cursor, queueTiming ? submission.receivedAt : now, now); + if (queueTiming) frame->CollectBackground(); frame->NoteFlipBatches(batchesAtFlip, unsignaledAtFlip); - CaptureTrace::Log("flip frame=%llu submission=%llu offset=%zu batch=%llu unsignaled=%llu", static_cast(frameSerial), static_cast(submission.serial), cursor, static_cast(batchesAtFlip), static_cast(unsignaledAtFlip)); + CaptureTrace::Log("flip frame=%llu submission=%llu offset=%zu batch=%llu unsignaled=%llu", static_cast(frameSerial.load()), static_cast(submission.serial), cursor, static_cast(batchesAtFlip), static_cast(unsignaledAtFlip)); flipHold.Release(); submission.flips.at(cursor)->GpuReady(frame); } else if (opcode == 0x15 || opcode == 0x16) { @@ -396,6 +422,7 @@ void Driver::execute(const Submission& submission) { } else if (drawPacket) { landPendingWaits(submission.queue); packetCounters.CountDraw(); + drawnSinceSeal() = true; bool drawn = false; timed(&WorkerProfile::drawMs, [&] { static const bool traceDraws = std::getenv("APS5_TRACE_DRAWS") != nullptr; @@ -419,6 +446,10 @@ void Driver::execute(const Submission& submission) { } else if (traceDraws) { std::fprintf(stderr, "[draw] target 0x%llx mask 0x%x ok\n",static_cast(color), readRegister(queue.context, 0x8e)); } + } catch (const RefusedDraw& refused) { + CaptureTrace::Log("draw-refused submission=%llu offset=%zu reason=%.256s", static_cast(submission.serial), cursor, refused.what()); + countSkip(Graphics::DrawSkip::Thrown); + ReportRefusedDraw(refused); } catch (const std::exception& error) { CaptureTrace::Log("draw-error submission=%llu offset=%zu reason=%.256s", static_cast(submission.serial), cursor, error.what()); countSkip(Graphics::DrawSkip::Thrown); @@ -438,10 +469,43 @@ void Driver::execute(const Submission& submission) { Pm4::Execute(packet, queue); } else if (sampleDump && !wroteOnGpu) { dumpSampleCounters(Pm4::SampleDumpAddress(packet)); + } else if (submission.queue == 0 && sealsWriteOnly(packet, opcode)) { + if (std::exchange(drawnSinceSeal(), false)) { + const auto seal = [this] { + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Flush); + std::lock_guard gpuLock(GuestMemory::GpuMutex()); + if (const auto localDevice = device.Load()) localDevice->SealPassWriteOnly(); + }; + if (pipelined && DrawPipeline::Queue0().Busy()) DrawPipeline::Queue0().Enqueue(seal, {}); + else seal(); + } } else if (opcode != 0x42 && opcode != 0x46 && opcode != 0x58) { if (!wroteOnGpu) { - if (Pm4::IndirectRegisterOpcode(opcode)) Pm4::ExecuteIndirectRegisters(packet, submission.registerLists.at(cursor), queue); - else Pm4::Execute(packet, queue); + if (Pm4::IndirectRegisterOpcode(opcode)) { + const auto& listed = submission.registerLists.at(cursor); + if (Pm4::IndirectRegistersValid(listed)) { + Pm4::ExecuteIndirectRegisters(packet, listed, queue); + } else { + landPendingWaits(submission.queue); + recordQueuedLabelsBeforeRead(submission.queue); + const auto current = Pm4::ReadIndirectRegisters(packet); + static const bool skipInvalid = std::getenv("APS5_SKIP_INVALID_INDIRECT_REGISTERS") != nullptr; + static std::atomic reported{false}; + if (!reported.exchange(true)) { + std::uint32_t invalid = 0; + for (std::size_t i = 0; i < listed.size(); i += 2) { + if (!Pm4::IndirectRegistersValid(std::span(&listed[i], 1))) { + invalid = listed[i]; + break; + } + } + std::fprintf(stderr, "[gpu] queue 0x%x %s at DWORD %zu: register list entry with offset dword 0x%08x read again where the packet executes%s\n", submission.queue, Pm4::Name(packet[0]).c_str(), cursor, invalid, skipInvalid ? "; invalid entries are skipped (APS5_SKIP_INVALID_INDIRECT_REGISTERS)" : ""); + } + Pm4::ExecuteIndirectRegisters(packet, current, queue, skipInvalid); + } + } else { + Pm4::Execute(packet, queue); + } if (opcode == 0x49 || opcode == 0x37) { if (const auto label = Pm4::DecodeLabelWrite(packet)) noteLabelStore(label->address, label->Bytes(), ++eventSerial, submission.queue); } @@ -468,6 +532,7 @@ void Driver::execute(const Submission& submission) { submitOpenWork(); wakeQueueZeroForCompletions(); }, {}); + endTiming.Finish(); executeRewindTail(submission); return; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp index bb8a031b38..d65d4bf795 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp @@ -11,6 +11,8 @@ #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/UnitShadow.hpp" +#include #include #include #include @@ -114,7 +116,29 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue if (!drainAll && !cpuStores && (opcode == 0x40 || opcode == 0x50 || opcode == 0x83)) { constexpr std::size_t gpuStoreLimit = 65536; bool drained = true; - if (const auto store = Pm4::ResolveStore(packet, queue, gpuStoreLimit)) { + const auto copy = opcode == 0x50 ? Pm4::DecodeMemoryCopy(packet) : std::nullopt; + const auto copyBehindPendingSource = [&] { + if (!copy.has_value() || copy->bytes > gpuStoreLimit || !GuestMemory::Accessible(reinterpret_cast(copy->source), copy->bytes) || !GuestMemory::Accessible(reinterpret_cast(copy->destination), copy->bytes, true)) return false; + const auto end = copy->source + copy->bytes; + const std::array, 1> range{{{copy->source, end}}}; + const auto& queued = deferredLabels().labels; + const bool labelQueued = std::any_of(queued.begin(), queued.end(), [&](const DeferredLabel& label) { return label.address < end && copy->source < label.address + label.size; }); + if (!labelQueued && !Graphics::Recorder::SnapshotWriteOverlaps(copy->source, copy->bytes) && !Graphics::StorageTexture::AnyPendingOverlaps(range) && !Graphics::AnyShadowedOverlaps(copy->source, copy->bytes)) return false; + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Copy); + std::lock_guard gpuLock(GuestMemory::GpuMutex()); + const auto localDevice = device.Load(); + if (localDevice == nullptr) return false; + recordDeferredLabels(localDevice.get(), submission.queue); + if (!localDevice->StoresPendingOver(copy->source, copy->bytes) || !localDevice->GpuCopyPossible(copy->destination, copy->source, copy->bytes)) return false; + const auto outcome = localDevice->CopyBuffer(copy->destination, copy->source, copy->bytes, 0, std::numeric_limits::max(), 0, 0, submission.queue, [](std::span, std::uint64_t) {}); + if (outcome.path != 1 && outcome.path != 3) throw std::runtime_error("AGC driver: a DMA_DATA copy of up to 64 KiB that GpuCopyPossible accepted took copy path " + std::to_string(outcome.path)); + return true; + }; + if (copyBehindPendingSource()) { + ++copiesBehindPendingSources; + wroteOnGpu = true; + drained = false; + } else if (const auto store = Pm4::ResolveStore(packet, queue, gpuStoreLimit)) { const auto bytes = store->Bytes(); if (bytes.empty()) { @@ -147,7 +171,6 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue } } if (drained && opcode == 0x50) { - const auto copy = Pm4::DecodeMemoryCopy(packet); if (copy.has_value() && copy->bytes > gpuStoreLimit && GuestMemory::Accessible(reinterpret_cast(copy->source), copy->bytes) && GuestMemory::Accessible(reinterpret_cast(copy->destination), copy->bytes, true)) { GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Copy); std::lock_guard gpuLock(GuestMemory::GpuMutex()); @@ -311,6 +334,7 @@ bool Driver::enqueueDmaPacket(std::span packet, std::uint32 const auto copy = Pm4::DecodeMemoryCopy(packet); if (!copy.has_value() || copy->destination % 4 != 0 || copy->bytes % 4 != 0 || !GuestMemory::Accessible(reinterpret_cast(copy->source), copy->bytes) || !GuestMemory::Accessible(reinterpret_cast(copy->destination), copy->bytes, true)) return false; DrawPipeline::Queue0().Enqueue([this, queue, copy = *copy] { + DrawPipeline::Queue0().ReleaseRunningReads(); GuestMemory::SetCurrentPacket(0x50, queue); constexpr std::size_t gpuStoreLimit = 65536; if (copy.bytes > gpuStoreLimit) { @@ -326,7 +350,7 @@ bool Driver::enqueueDmaPacket(std::span packet, std::uint32 std::vector bytes(copy.bytes); GuestMemory::Read(copy.source, bytes, 1); commitDmaStore(queue, copy.destination, bytes); - }, {{copy->destination, copy->destination + copy->bytes}}); + }, {{copy->destination, copy->destination + copy->bytes}}, 0, {}, {{copy->source, copy->source + copy->bytes}}); return true; } 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 ad0dfa3e8d..b69bccd8fe 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" @@ -32,6 +33,13 @@ #include #include #include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#endif #ifdef __GLIBC__ #include #endif @@ -69,6 +77,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 +94,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 +115,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 { @@ -410,6 +425,36 @@ static bool RelocatedRegisters(const ShaderSnapshot& snapshot, Shader used, cons return std::memcmp(static_cast(bound), snapshot.header.data() + (address - snapshot.headerAddress), bytes) == 0; } +// A title can relocate a registered header, and the arrays it points at, into memory of its own and resolve the ABI +// with that copy. Every array the copy refers to must still hold the registered bytes: the registered header then +// decodes to the values the copy describes. +static bool ReferredBytesMatch(const ShaderSnapshot& snapshot, const void* pointer, const void* registeredPointer, std::size_t bytes) { + if (bytes == 0 || pointer == registeredPointer) return true; + if (pointer == nullptr || registeredPointer == nullptr) return false; + const auto address = reinterpret_cast(registeredPointer); + if (address < snapshot.headerAddress) return false; + const auto offset = address - snapshot.headerAddress; + if (offset > snapshot.header.size() || snapshot.header.size() - offset < bytes) return false; + if (!GuestMemory::Accessible(pointer, bytes)) return false; + return std::memcmp(static_cast(pointer), snapshot.header.data() + offset, bytes) == 0; +} + +static bool RelocatedCopy(const ShaderSnapshot& snapshot, Shader used, const Shader& registered) { + Shader masked = used; + masked.user_data = registered.user_data; + masked.cx_registers = registered.cx_registers; + masked.sh_registers = registered.sh_registers; + masked.specials = registered.specials; + masked.input_semantics = registered.input_semantics; + masked.output_semantics = registered.output_semantics; + if (std::memcmp(&masked, ®istered, offsetof(Shader, num_sh_registers) + sizeof(Shader::num_sh_registers)) != 0) return false; + return ReferredBytesMatch(snapshot, used.sh_registers, registered.sh_registers, used.num_sh_registers * sizeof(ShaderRegister)) + && ReferredBytesMatch(snapshot, used.cx_registers, registered.cx_registers, used.num_cx_registers * sizeof(ShaderRegister)) + && ReferredBytesMatch(snapshot, used.specials, registered.specials, sizeof(ShaderSpecialRegs)) + && ReferredBytesMatch(snapshot, used.input_semantics, registered.input_semantics, used.num_input_semantics * sizeof(ShaderSemantic)) + && ReferredBytesMatch(snapshot, used.output_semantics, registered.output_semantics, used.num_output_semantics * sizeof(ShaderSemantic)); +} + bool SameHeader(const ShaderSnapshot& snapshot, const Shader* shader) { if (snapshot.headerAddress == reinterpret_cast(shader)) return true; if (snapshot.header.size() < sizeof(Shader)) return false; @@ -419,7 +464,7 @@ bool SameHeader(const ShaderSnapshot& snapshot, const Shader* shader) { std::memcpy(&used, static_cast(shader), sizeof(Shader)); std::memcpy(®istered, snapshot.header.data(), sizeof(Shader)); used.user_data = registered.user_data; - return std::memcmp(&used, ®istered, offsetof(Shader, num_sh_registers) + sizeof(Shader::num_sh_registers)) == 0 || RelocatedRegisters(snapshot, used, registered) || FusedHullHeader(snapshot, used, registered); + return std::memcmp(&used, ®istered, offsetof(Shader, num_sh_registers) + sizeof(Shader::num_sh_registers)) == 0 || RelocatedRegisters(snapshot, used, registered) || RelocatedCopy(snapshot, used, registered) || FusedHullHeader(snapshot, used, registered); } std::size_t ShaderRegistry::chunkFor(std::uint64_t address) const { @@ -773,7 +818,7 @@ std::optional DecodeFloatMode(const Registers case 0: rsrc1 = 0x212; fp16OverflowBit = 26; break; case 1: rsrc1 = 0x00a; fp16OverflowBit = 29; break; case 2: case 4: case 6: rsrc1 = 0x08a; fp16OverflowBit = 31; break; - case 5: case 7: rsrc1 = 0x10a; fp16OverflowBit = 30; break; + case 3: case 5: case 7: rsrc1 = 0x10a; fp16OverflowBit = 30; break; default: return std::nullopt; } const auto found = registers.find(rsrc1); @@ -871,7 +916,7 @@ std::unique_ptr PlanRegistered(const ShaderSnapshot& snap case 1: stage = Stage::Fragment; programRegister = 0x008; resourceRegister = 0x00b; break; case 2: stage = Stage::Vertex; programRegister = 0x0c8; resourceRegister = 0x08b; firstUser = 8; break; case 4: stage = Stage::Mesh; programRegister = 0x0c8; resourceRegister = 0x08b; break; - case 5: return {}; + case 3: case 5: return {}; case 6: stage = Stage::Mesh; programRegister = 0x088; resourceRegister = 0x08b; break; case 7: stage = Stage::TessellationControl; programRegister = 0x108; resourceRegister = 0x10b; break; default: @@ -1160,7 +1205,7 @@ public: } void Close() { - std::vector joined; + std::vector joined; { std::unique_lock lock(mutex); closed = true; @@ -1168,16 +1213,45 @@ public: joined.swap(workers); } ready.notify_all(); - for (const auto worker : joined) { - if (!pthread_equal(worker, pthread_self())) pthread_join(worker, nullptr); - } + for (const auto& worker : joined) Join(worker); } private: + static constexpr std::size_t StackBytes = 64u << 20u; + +#ifdef _WIN32 + struct Worker { + HANDLE handle; + unsigned id; + }; + + static void Join(const Worker& worker) { + if (worker.id != GetCurrentThreadId()) WaitForSingleObject(worker.handle, INFINITE); + CloseHandle(worker.handle); + } +#else + using Worker = pthread_t; + + static void Join(Worker worker) { + if (!pthread_equal(worker, pthread_self())) pthread_join(worker, nullptr); + } +#endif + explicit RegistrationPreparePool(unsigned count) { +#ifdef _WIN32 + for (unsigned index = 0; index < count; ++index) { + unsigned id = 0; + const auto thread = _beginthreadex(nullptr, static_cast(StackBytes), &RegistrationPreparePool::WindowsMain, this, STACK_SIZE_PARAM_IS_A_RESERVATION, &id); + if (thread != 0) { + workers.push_back({reinterpret_cast(thread), id}); + ++threads; + liveWorkers.fetch_add(1, std::memory_order_acq_rel); + } + } +#else pthread_attr_t attributes; pthread_attr_init(&attributes); - pthread_attr_setstacksize(&attributes, 64u << 20u); + pthread_attr_setstacksize(&attributes, StackBytes); for (unsigned index = 0; index < count; ++index) { pthread_t thread; if (pthread_create(&thread, &attributes, &RegistrationPreparePool::Main, this) == 0) { @@ -1187,8 +1261,16 @@ private: } } pthread_attr_destroy(&attributes); +#endif } +#ifdef _WIN32 + static unsigned __stdcall WindowsMain(void* argument) { + Main(argument); + return 0; + } +#endif + static void* Main(void* argument) { auto& pool = *static_cast(argument); if (PerformanceTrace::Configured()) PerformanceTrace::BindThread("shader prepare", TraceThread::DefaultCapacity / 8); @@ -1233,7 +1315,7 @@ private: std::condition_variable ready; std::condition_variable idle; std::deque tasks; - std::vector workers; + std::vector workers; unsigned threads = 0; inline static std::atomic liveWorkers{0}; unsigned running = 0; @@ -1320,13 +1402,13 @@ std::vector PrepareGraphicsStages(const DrawDecode& decod require(expected == decoded.roles, "graphics ABI linked programs do not match the stage routing"); std::vector linked; std::vector memory; + AppendProgramMemory(memory, decoded.programs); for (std::size_t index = 0; index < decoded.programs.size(); ++index) { const auto& program = decoded.programs[index]; require(program.snapshot != nullptr && program.codeOffset < program.snapshot->code.size(), "graphics ABI has an invalid registered entry point"); const auto code = std::span(program.snapshot->code).subspan(program.codeOffset); require(program.binary.code.data() == code.data() && program.binary.code.size() == code.size() && program.binary.codeAddress == program.snapshot->codeAddress + program.codeOffset * sizeof(std::uint32_t), "graphics ABI entry point differs from its snapshot"); linked.push_back({decoded.roles[index], program.binary, program.userDataBase, program.firstUserSgpr, program.userData}); - memory.insert(memory.end(), program.memory.begin(), program.memory.end()); } std::vector prepared; std::uint32_t pushOffset = 0; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/SynchronizationStatistics.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/SynchronizationStatistics.cpp index f63a77d18c..5b1e22624b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/SynchronizationStatistics.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/SynchronizationStatistics.cpp @@ -21,7 +21,7 @@ std::atomic packetLockRecordUs{0}, packetLockSubmitUs{0}; std::atomic triesFailed[TrySites] = {}; -std::atomic storesOnGpu{0}, storesBehindCompletions{0}, storesOnCpu{0}, storesDrained{0}, dmaFills{0}, copiesToHostMemory{0}; +std::atomic storesOnGpu{0}, storesBehindCompletions{0}, storesOnCpu{0}, storesDrained{0}, dmaFills{0}, copiesToHostMemory{0}, copiesBehindPendingSources{0}; std::map drainCounts; @@ -39,7 +39,7 @@ SubmissionCosts& submissionCosts(std::uint32_t queue) { void reportSync() { std::string report; for (const auto& [code, count] : drainCounts) report += " " + (code == 0xffffu ? std::string("flip") : Pm4::Name(code << 8u)) + "=" + std::to_string(count); - AgcDriver::ProfilePrint_nid_no_patch("[sync] %llu device drains by packet:%s (%llu waited without the GPU mutex); %llu labels written on the GPU, %llu deferred behind completions, %llu no-op, fallbacks: idle %llu, completions %llu, not imported %llu, undecodable %llu; %llu labels queued per worker, recorded in %llu groups, %llu recorded at once; stores: %llu recorded on the GPU, %llu behind completions, %llu on the CPU (idle), %llu synced (drained), %llu DMA fills stored as buffer fills; %llu copies into host memory synced on their source\n", static_cast(drainTotal), report.c_str(), static_cast(unlockedDrains.load()), static_cast(gpuLabels.load()), static_cast(completionLabels.load()), static_cast(noOpLabels.load()), static_cast(labelFallbacks[1].load()), static_cast(labelFallbacks[2].load()), static_cast(labelFallbacks[3].load()), static_cast(labelFallbacks[4].load()), static_cast(queuedLabels.load()), static_cast(labelGroups.load()), static_cast(immediateLabels.load()), static_cast(storesOnGpu.load()), static_cast(storesBehindCompletions.load()), static_cast(storesOnCpu.load()), static_cast(storesDrained.load()), static_cast(dmaFills.load()), static_cast(copiesToHostMemory.load())); + AgcDriver::ProfilePrint_nid_no_patch("[sync] %llu device drains by packet:%s (%llu waited without the GPU mutex); %llu labels written on the GPU, %llu deferred behind completions, %llu no-op, fallbacks: idle %llu, completions %llu, not imported %llu, undecodable %llu; %llu labels queued per worker, recorded in %llu groups, %llu recorded at once; stores: %llu recorded on the GPU, %llu behind completions, %llu on the CPU (idle), %llu synced (drained), %llu DMA fills stored as buffer fills; %llu copies into host memory synced on their source; %llu copies of up to 64 KiB recorded on the GPU behind their source's pending stores\n", static_cast(drainTotal), report.c_str(), static_cast(unlockedDrains.load()), static_cast(gpuLabels.load()), static_cast(completionLabels.load()), static_cast(noOpLabels.load()), static_cast(labelFallbacks[1].load()), static_cast(labelFallbacks[2].load()), static_cast(labelFallbacks[3].load()), static_cast(labelFallbacks[4].load()), static_cast(queuedLabels.load()), static_cast(labelGroups.load()), static_cast(immediateLabels.load()), static_cast(storesOnGpu.load()), static_cast(storesBehindCompletions.load()), static_cast(storesOnCpu.load()), static_cast(storesDrained.load()), static_cast(dmaFills.load()), static_cast(copiesToHostMemory.load()), static_cast(copiesBehindPendingSources.load())); } void countLabelOutcome(int reason) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp index 2254d5e6a8..6aead1a2cc 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp @@ -329,6 +329,7 @@ void Driver::waitMemory(std::span packet, std::uint32_t que ~BlockedWait() { driver.noteWaitBlocked(queue, 0, false); } } blocked{*this, queue}; static const bool pauseSpin = std::getenv("APS5_NO_PAUSE_SPIN") == nullptr; + static const bool storeHeldResults = std::getenv("APS5_NO_QUEUED_DRAW_READ_ORDER") == nullptr; const auto spinLimit = queue == 0 ? std::chrono::microseconds(1500) : std::chrono::microseconds(100); const auto spinStart = std::chrono::steady_clock::now(); bool spinning = pauseSpin; @@ -352,6 +353,7 @@ void Driver::waitMemory(std::span packet, std::uint32_t que } CheckFailure(); if (pollService(!spinning)) return; + if (storeHeldResults && !spinning && (polls & 31u) == 0) GuestMemory::FlushGpuWrites(awaited, awaitedBytes); noteAwaitingTitle(queue, awaited); if (requireMemory && (polls & 15u) == 0) { std::unique_lock settleLock(GuestMemory::GpuMutex(), std::try_to_lock); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index 6ea26ec9ab..6e7a43a575 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); @@ -810,27 +837,22 @@ struct WriteTracker { bool initialized = false; bool watched = false; static constexpr std::size_t CoarseBlocks = 64; -#ifdef _WIN32 - std::uintptr_t base = 0; - std::size_t size = 0; - // Generation of the last collected write per 64 KiB block of the arena. - std::vector blocks; - // The same, stamped by collects only (dirty pages = CPU stores), never by MarkWritten: the - // late label rule asks whether the CPU touched a label's block, and the driver's own GPU label - // records in the same block (the title's per-job slots are 0x20 apart) must not count. - std::vector cpuBlocks; - std::vector coarseBlocks; - WriteWatchCoverage coverage; - std::vector hintBlocks; -#else static constexpr std::size_t LeafBlocks = std::size_t{1} << 16; - static constexpr std::size_t LeafCount = std::size_t{1} << 15; struct Leaf { std::array blocks{}; std::array cpuBlocks{}; std::array coarseBlocks{}; +#ifdef _WIN32 + std::array hintBlocks{}; +#endif }; std::vector> leaves; +#ifdef _WIN32 + std::uintptr_t base = 0; + std::size_t size = 0; + WriteWatchCoverage coverage; +#else + static constexpr std::size_t LeafCount = std::size_t{1} << 15; #endif // Atomic only so a per-thread memo hit can read it without the mutex (every mutation and every // stamp still happen under it): a hit must return the current value, including MarkWritten @@ -862,11 +884,8 @@ struct WriteTracker { GuestArena::GuestArenaRange_nid_postfix(&base, &size); watched = size != 0 && GuestArena::GuestArenaWriteWatched_nid_postfix(); if (!watched) return; - blocks.assign(size / WriteBlockBytes + 1, 0); - cpuBlocks.assign(size / WriteBlockBytes + 1, 0); - coarseBlocks.assign(blocks.size() / CoarseBlocks + 1, 0); + leaves.resize((size / WriteBlockBytes + LeafBlocks) / LeafBlocks); coverage.Initialize(base, size); - hintBlocks.assign(size / WriteBlockBytes + 1, 0); pages.resize(1u << 16); GuestArena::GuestArenaSetPrivateMappingObserver_nid_postfix(&watchPrivateMapping); #else @@ -906,24 +925,24 @@ struct WriteTracker { if (first >= stop) return; bool started = false; for (auto block = blockOf(first); block <= blockOf(stop - 1); ++block) { - if (!coverage.Covers(blockBegin(block), 1)) continue; + if (!coverage.Covers(blockBegin(block), static_cast(std::min(WriteBlockBytes, base + size - blockBegin(block))))) continue; if (!started) ++generation; started = true; - hintBlocks[block] = generation; + stampHint(block); } } bool hinted(std::uint64_t first, std::uint64_t stop) const { if (first >= stop || first < base || stop > base + size) return false; for (auto block = blockOf(first); block <= blockOf(stop - 1); ++block) { - if (hintBlocks[block] == 0 || coverage.Covers(blockBegin(block), 1)) return false; + if (hintOf(block) == 0 || coverage.Covers(blockBegin(block), static_cast(std::min(WriteBlockBytes, base + size - blockBegin(block))))) return false; } return !GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(static_cast(first), static_cast(stop - first)); } std::uint32_t newestHint(std::uint64_t first, std::uint64_t stop) const { std::uint32_t newest = 0; - for (auto block = blockOf(first); block <= blockOf(stop - 1); ++block) newest = std::max(newest, hintBlocks[block]); + for (auto block = blockOf(first); block <= blockOf(stop - 1); ++block) newest = std::max(newest, hintOf(block)); return newest; } #endif @@ -1024,13 +1043,26 @@ struct WriteTracker { slot = {stampGeneration, from, to}; } - std::uint32_t stampOf(std::uint64_t block) const { + Leaf& leafFor(std::uint64_t block) { + auto& leaf = leaves[block / LeafBlocks]; + if (leaf == nullptr) leaf = std::make_unique(); + return *leaf; + } + #ifdef _WIN32 - return blocks[block]; -#else + void stampHint(std::uint64_t block) { + leafFor(block).hintBlocks[block % LeafBlocks] = generation; + } + + std::uint32_t hintOf(std::uint64_t block) const { + const auto& leaf = leaves[block / LeafBlocks]; + return leaf != nullptr ? leaf->hintBlocks[block % LeafBlocks] : 0; + } +#endif + + std::uint32_t stampOf(std::uint64_t block) const { const auto& leaf = leaves[block / LeafBlocks]; return leaf != nullptr ? leaf->blocks[block % LeafBlocks] : 0; -#endif } static constexpr std::size_t UnstampedSlots = 1u << 16; @@ -1068,28 +1100,23 @@ struct WriteTracker { } std::uint32_t coarseStampOf(std::uint64_t block) const { -#ifdef _WIN32 - return coarseBlocks[block / CoarseBlocks]; -#else const auto& leaf = leaves[block / LeafBlocks]; return leaf != nullptr ? leaf->coarseBlocks[(block % LeafBlocks) / CoarseBlocks] : 0; -#endif } bool unchanged(std::uint64_t first, std::uint64_t last, std::uint64_t since) const { if (first == last) return stampOf(first) <= since; const auto newer = [since](std::uint32_t stamp) { return stamp > since; }; while (first <= last) { + const auto& leaf = leaves[first / LeafBlocks]; + if (leaf == nullptr) { + first = (first / LeafBlocks + 1) * LeafBlocks; + continue; + } const auto end = std::min(last + 1, (first / CoarseBlocks + 1) * CoarseBlocks); if (end - first < CoarseBlocks || coarseStampOf(first) > since) { -#ifdef _WIN32 - if (std::any_of(blocks.begin() + first, blocks.begin() + end, newer)) return false; -#else - if (const auto& leaf = leaves[first / LeafBlocks]; leaf != nullptr) { - const auto begin = leaf->blocks.begin() + first % LeafBlocks; - if (std::any_of(begin, begin + (end - first), newer)) return false; - } -#endif + const auto begin = leaf->blocks.begin() + first % LeafBlocks; + if (std::any_of(begin, begin + (end - first), newer)) return false; } first = end; } @@ -1097,12 +1124,8 @@ struct WriteTracker { } std::uint32_t cpuStampOf(std::uint64_t block) const { -#ifdef _WIN32 - return cpuBlocks[block]; -#else const auto& leaf = leaves[block / LeafBlocks]; return leaf != nullptr ? leaf->cpuBlocks[block % LeafBlocks] : 0; -#endif } bool storedOver(std::uint64_t address, std::uint64_t end, std::uint64_t generation, bool walked) const { @@ -1143,23 +1166,13 @@ struct WriteTracker { void stamp(std::uint64_t block, std::uint32_t stampGeneration, StampKind kind) { const bool cpu = kind == StampKind::Cpu || kind == StampKind::ImportWindow; const bool stored = kind != StampKind::ImportWindow; -#ifdef _WIN32 + auto& leaf = leafFor(block); if (stored) { - blocks[block] = stampGeneration; - auto& coarse = coarseBlocks[block / CoarseBlocks]; + leaf.blocks[block % LeafBlocks] = stampGeneration; + auto& coarse = leaf.coarseBlocks[(block % LeafBlocks) / CoarseBlocks]; coarse = std::max(coarse, stampGeneration); } - if (cpu) cpuBlocks[block] = stampGeneration; -#else - auto& leaf = leaves[block / LeafBlocks]; - if (leaf == nullptr) leaf = std::make_unique(); - if (stored) { - leaf->blocks[block % LeafBlocks] = stampGeneration; - auto& coarse = leaf->coarseBlocks[(block % LeafBlocks) / CoarseBlocks]; - coarse = std::max(coarse, stampGeneration); - } - if (cpu) leaf->cpuBlocks[block % LeafBlocks] = stampGeneration; -#endif + if (cpu) leaf.cpuBlocks[block % LeafBlocks] = stampGeneration; } }; @@ -1191,6 +1204,7 @@ void watchPrivateMapping(std::uintptr_t address, std::size_t bytes, std::uint64_ ++tracker.generation; tracker.noteCpuStore(address, address + bytes, tracker.generation); for (auto block = tracker.blockOf(address); block <= tracker.blockOf(address + bytes - 1); ++block) { + if (const auto& leaf = tracker.leaves[block / WriteTracker::LeafBlocks]; leaf != nullptr) leaf->hintBlocks[block % WriteTracker::LeafBlocks] = 0; if (const auto found = tracker.driverPieces.find(block); found != tracker.driverPieces.end()) found->second.dropped = std::max(found->second.dropped, tracker.generation); tracker.stamp(block, tracker.generation, StampKind::Cpu); } @@ -1279,9 +1293,14 @@ void stampWrittenRun(void* context, std::uintptr_t begin, std::uintptr_t end) { } #ifdef _WIN32 +std::atomic freshCollectHook{nullptr}; + bool collectWindowsWrites(StampRuns& runs, std::uint64_t first, std::uint64_t stop, bool armedOnly) { auto& tracker = runs.tracker; const bool fresh = runs.kind == StampKind::Fresh; + if (fresh) { + if (const auto hook = freshCollectHook.load(std::memory_order_acquire)) return hook(static_cast(first), static_cast(stop - first), &stampWrittenRun, &runs); + } const auto collect = fresh ? &GuestArena::GuestArenaCollectFreshWrites_nid_postfix : armedOnly ? &GuestArena::GuestArenaCollectArmedWrites_nid_postfix : &GuestArena::GuestArenaCollectWrites_nid_postfix; static const bool singlePass = std::getenv("APS5_NO_SINGLE_PASS_COLLECT") == nullptr; const auto walk = [&](bool clear) { @@ -1465,6 +1484,12 @@ void SetUnwatchHook(void (*hook)(std::uint64_t, std::size_t)) { unwatchHook.store(hook, std::memory_order_release); } +#ifdef _WIN32 +void SetFreshCollectHook(FreshCollectHook hook) { + freshCollectHook.store(hook, std::memory_order_release); +} +#endif + void Unwatch(std::uint64_t address, std::size_t bytes) { if (bytes == 0 || bytes > std::numeric_limits::max() - address) return; beforeUnwatch(address, bytes); @@ -1517,39 +1542,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; } @@ -1660,7 +1701,7 @@ std::uint64_t StoreOwnBytes(std::uint64_t address, std::span so std::uint64_t MarkWritten(std::uint64_t address, std::size_t bytes) { auto& tracker = Tracker(); - const auto lock = lockTracker(tracker); + auto lock = lockTracker(tracker); tracker.initialize(); if (bytes == 0) return 0; ++tracker.generation; @@ -1679,6 +1720,9 @@ std::uint64_t MarkWritten(std::uint64_t address, std::size_t bytes) { const bool complete = GuestWriteWatch::GuestWriteWatchCollectFresh_nid_postfix(static_cast(blockFirst), static_cast(blockStop - blockFirst), &stampWrittenRun, &runs); #endif if (!complete) { + lock.unlock(); + beforeUnwatch(blockFirst, static_cast(blockStop - blockFirst)); + lock.lock(); unwatchLocked(tracker, blockFirst, static_cast(blockStop - blockFirst)); tracker.noteUnstampedStore(address, address + bytes); return 0; @@ -1760,7 +1804,7 @@ bool walkHints(WriteTracker& tracker, std::uint64_t first, std::uint64_t stop) { if (!GuestArena::GuestArenaCollectWrites_nid_postfix(static_cast(cursor), static_cast(stop - cursor), tracker.pages.data(), &count, true)) return false; if (count != 0 && !marked) ++tracker.generation; marked = marked || count != 0; - for (std::size_t i = 0; i < count; ++i) tracker.hintBlocks[tracker.blockOf(reinterpret_cast(tracker.pages[i]))] = tracker.generation; + for (std::size_t i = 0; i < count; ++i) tracker.stampHint(tracker.blockOf(reinterpret_cast(tracker.pages[i]))); if (count < tracker.pages.size()) break; cursor = reinterpret_cast(tracker.pages[count - 1]) + WritePageBytes; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/HostHdr.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/HostHdr.cpp index d18386b591..63748fa18c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/HostHdr.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/HostHdr.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Execution/include/HostHdr.hpp" #include #include +#include #include namespace AgcDriver { @@ -24,7 +25,7 @@ bool SameDisplay(const HostDisplayHdr& a, const HostDisplayHdr& b) { void Publish(HostHdrStatus& status, const HostDisplayHdr& display, bool surfaceHdr10) { const bool displayChanged = !SameDisplay(status.display, display); - const bool available = display.hdr && surfaceHdr10; + const bool available = display.hdr && surfaceHdr10 && std::getenv("APS5_FORCE_SDR") == nullptr; const bool availabilityChanged = available != status.available; status.display = display; status.surfaceHdr10 = surfaceHdr10; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp index e21ddf0599..ca74a794d9 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp @@ -538,14 +538,8 @@ PerformanceOverlay::PerformanceOverlay(const Graphics::Context& context, std::si layoutInfo.pushConstantRangeCount = 1; layoutInfo.pPushConstantRanges = &push; Graphics::Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &pipelineLayout), "vkCreatePipelineLayout performance overlay"); - const auto createModule = context.Function("vkCreateShaderModule"); - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = sizeof(PERFORMANCE_OVERLAY_VERT_SPV); - moduleInfo.pCode = PERFORMANCE_OVERLAY_VERT_SPV; - Graphics::Check(createModule(context.device, &moduleInfo, nullptr, &vertex), "vkCreateShaderModule performance overlay vertex"); - moduleInfo.codeSize = sizeof(PERFORMANCE_OVERLAY_FRAG_SPV); - moduleInfo.pCode = PERFORMANCE_OVERLAY_FRAG_SPV; - Graphics::Check(createModule(context.device, &moduleInfo, nullptr, &fragment), "vkCreateShaderModule performance overlay fragment"); + vertex = Graphics::CreateShaderModule(context, PERFORMANCE_OVERLAY_VERT_SPV, sizeof(PERFORMANCE_OVERLAY_VERT_SPV), "vkCreateShaderModule performance overlay vertex"); + fragment = Graphics::CreateShaderModule(context, PERFORMANCE_OVERLAY_FRAG_SPV, sizeof(PERFORMANCE_OVERLAY_FRAG_SPV), "vkCreateShaderModule performance overlay fragment"); } catch (...) { release(); throw; 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/Pm4.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp index bab78881d7..20b73f581a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp @@ -276,7 +276,7 @@ void Validate(std::span packet, std::uint32_t queue) { break; } case 0x0c: break; - case 0x17: graphics(); size(6); break; + case 0x17: size(6); break; case 0x1a: graphics(); require(packet.size() == 3 || packet.size() == 5, "invalid context-state packet size"); @@ -945,15 +945,29 @@ void NoteDispatchRegisters(std::span packet, QueueState& qu } } -void ExecuteIndirectRegisters(std::span packet, std::span pairs, QueueState& queue) { +bool IndirectRegistersValid(std::span pairs) { + for (std::size_t i = 0; i < pairs.size(); i += 2) { + if (pairs[i] == 0xffffffffu || (pairs[i] & ~0x70000000u) > 0xffffu) return false; + } + return true; +} + +void ExecuteIndirectRegisters(std::span packet, std::span pairs, QueueState& queue, bool skipInvalid) { const auto opcode = (packet[0] >> 8u) & 0xffu; require(packet.size() == 5 && IndirectRegisterOpcode(opcode), "expected indirect register packet"); require(pairs.size() == static_cast(packet[4]) * 2, "indirect register list does not match its packet"); - for (std::size_t i = 0; i < pairs.size(); i += 2) registerOffset(pairs[i]); - for (std::size_t i = 0; i < pairs.size(); i += 2) writeRegister(queue, opcode, registerOffset(pairs[i]), pairs[i + 1]); + const auto usable = [&](std::uint32_t value) { return !skipInvalid || IndirectRegistersValid(std::span(&value, 1)); }; + for (std::size_t i = 0; i < pairs.size(); i += 2) { + if (usable(pairs[i])) registerOffset(pairs[i]); + } + for (std::size_t i = 0; i < pairs.size(); i += 2) { + if (usable(pairs[i])) writeRegister(queue, opcode, registerOffset(pairs[i]), pairs[i + 1]); + } if (queue.savedContext.has_value() && TraceContextState()) { std::fprintf(stderr, "[context] indirect 0x%x:", opcode); - for (std::size_t i = 0; i < pairs.size(); i += 2) std::fprintf(stderr, " %x", registerOffset(pairs[i])); + for (std::size_t i = 0; i < pairs.size(); i += 2) { + if (usable(pairs[i])) std::fprintf(stderr, " %x", registerOffset(pairs[i])); + } std::fprintf(stderr, "\n"); } } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PresentationPass.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PresentationPass.cpp index ecaa69ff9c..e42e4e2b03 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/PresentationPass.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PresentationPass.cpp @@ -70,14 +70,8 @@ PresentationPass::PresentationPass(const Graphics::Context& context, std::size_t tableWrites[i].pBufferInfo = &tableInfo; } context.Function("vkUpdateDescriptorSets")(context.device, static_cast(tableWrites.size()), tableWrites.data(), 0, nullptr); - const auto createModule = context.Function("vkCreateShaderModule"); - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = sizeof(PRESENTATION_PASS_VERT_SPV); - moduleInfo.pCode = PRESENTATION_PASS_VERT_SPV; - Graphics::Check(createModule(context.device, &moduleInfo, nullptr, &vertex), "vkCreateShaderModule presentation vertex"); - moduleInfo.codeSize = sizeof(PRESENTATION_PASS_FRAG_SPV); - moduleInfo.pCode = PRESENTATION_PASS_FRAG_SPV; - Graphics::Check(createModule(context.device, &moduleInfo, nullptr, &fragment), "vkCreateShaderModule presentation fragment"); + vertex = Graphics::CreateShaderModule(context, PRESENTATION_PASS_VERT_SPV, sizeof(PRESENTATION_PASS_VERT_SPV), "vkCreateShaderModule presentation vertex"); + fragment = Graphics::CreateShaderModule(context, PRESENTATION_PASS_FRAG_SPV, sizeof(PRESENTATION_PASS_FRAG_SPV), "vkCreateShaderModule presentation fragment"); } catch (...) { release(); throw; 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 1ebaafcc12..dd23f17509 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -15,21 +15,26 @@ #include "prx/libSceAgcDriver/Execution/include/BdaFeatures.hpp" #include "prx/libSceAgcDriver/Execution/include/DeviceFault.hpp" #include "prx/libSceAgcDriver/Execution/include/FragmentBarycentric.hpp" +#include "prx/libSceAgcDriver/Execution/include/Float32FlushControl.hpp" #include "prx/libSceAgcDriver/Execution/include/PipelineLinkOptimization.hpp" #include "prx/libSceAgcDriver/Execution/include/DrawInputsInPlace.hpp" #include "prx/libSceAgcDriver/Execution/include/RobustBufferAccess.hpp" +#include "prx/libSceAgcDriver/Execution/include/SubgroupBarrier.hpp" #include "prx/libSceAgcDriver/Execution/include/SubgroupClock.hpp" #include "prx/libSceAgcDriver/Execution/include/ShaderDeviceProfile.hpp" #include "prx/libSceAgcDriver/Execution/include/PresentMode.hpp" #include "prx/libSceAgcDriver/Execution/include/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" #include "prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp" #include "prx/libSceAgcDriver/Execution/include/HostHdr.hpp" #include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" +#include "prx/libSceAgcDriver/Graphics/include/SampleOrder.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" #include "prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp" #include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" @@ -62,6 +67,7 @@ #include #include #include +#include #include #include #include @@ -155,24 +161,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 +199,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(); } @@ -311,10 +324,12 @@ struct VulkanDevice::State { bool float16DenormPreserve = false; bool float32FlushToZeroCandidate = false; bool float32FlushToZero = false; + bool float32DenormPreserve = false; bool probingFloatControls = false; bool demoteToHelperInvocation = false; bool earlyFragmentSampleMaskTestBeforeSampleCounting = false; bool robustBufferAccess2 = false; + bool subgroupControlBarriers = true; // VK_EXT_descriptor_indexing with non-uniform image array indexing (bindless image tables in // graphics stages, and compute workgroups wider than a wave). bool descriptorIndexing = false; @@ -334,11 +349,15 @@ struct VulkanDevice::State { bool imageViewMinLod = false; bool pipelineExecutableInfo = false; bool maintenance8 = false; + bool maintenance5 = false; + bool fillModeNonSolid = false; std::uint32_t srgbDecodeFormats = 0; std::uint32_t srgbCopyFormats = 0; bool storageImageWithoutFormat = false; VkSampleCountFlags sampleLocationSampleCounts = 0; VkSampleCountFlags sampleLocationGridSampleCounts = 0; + std::array sampleLocationGridSizes{}; + bool shaderStencilExport = false; bool depthClamp = false; bool depthBounds = false; bool depthBiasClamp = false; @@ -449,6 +468,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 +500,7 @@ struct VulkanDevice::State { bool queuePending = false; std::uint32_t queueIndex = 0; int nextDumpIndex = 0; + bool nextDumpScreenshot = false; FrameDumpWriter dumpWriter; template @@ -678,6 +699,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 +714,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 @@ -757,6 +783,7 @@ struct VulkanDevice::State { Graphics::ClearCachedPipelines(device); Graphics::ClearDepthSurfaces(device); Graphics::ClearMultisampleTargets(device); + Graphics::ClearSampleOrders(device); { std::lock_guard pipelines(computePipelinesMutex); computePipelines.clear(); @@ -898,10 +925,7 @@ ComputePipelineReplayResult VulkanDevice::ReplayComputePipeline(const ComputePip check(state->DeviceFunction("vkCreatePipelineCache")(state->device, &cacheInfo, nullptr, &replay.cache), "vkCreatePipelineCache replay"); result.cacheMilliseconds = elapsed(phase); phase = std::chrono::steady_clock::now(); - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = capture.spirv.size() * sizeof(std::uint32_t); - moduleInfo.pCode = capture.spirv.data(); - check(state->DeviceFunction("vkCreateShaderModule")(state->device, &moduleInfo, nullptr, &objects.module), "vkCreateShaderModule replay"); + objects.module = Graphics::CreateShaderModule(state->DeviceFunction("vkCreateShaderModule"), state->device, state->graphicsPipelineLibrary, capture.spirv.data(), capture.spirv.size() * sizeof(std::uint32_t), "vkCreateShaderModule replay"); result.moduleMilliseconds = elapsed(phase); phase = std::chrono::steady_clock::now(); std::array, 2> descriptorCounts{}; @@ -1145,12 +1169,15 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : std::vector availableExtensions(extensionCount); check(enumerateDeviceExtensions(selected, nullptr, &extensionCount, availableExtensions.data()), "vkEnumerateDeviceExtensionProperties"); const auto hasExtension = [&](const char* name) { return std::any_of(availableExtensions.begin(), availableExtensions.end(), [&](const auto& item) { return std::strcmp(item.extensionName, name) == 0; }); }; + VkDriverId driverId{}; if (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}; state->InstanceFunction("vkGetPhysicalDeviceProperties2")(selected, &driverProperties); + driverId = driver.driverID; state->driverInfo = std::string(driver.driverName) + " " + driver.driverInfo; state->individuallyFreeDescriptorSets = driver.driverID == VK_DRIVER_ID_MESA_LLVMPIPE; + state->subgroupControlBarriers = SubgroupControlBarrierUsable(driver.driverID); } auto byteFeatures = QueryBdaByteFeatures(selected, state->InstanceFunction("vkGetPhysicalDeviceFeatures2"), availableExtensions); auto bdaFeatures = QueryBdaFeatures(selected, state->InstanceFunction("vkGetPhysicalDeviceFeatures2"), availableExtensions); @@ -1248,6 +1275,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : deviceExtensions.push_back(VK_EXT_SHADER_STENCIL_EXPORT_EXTENSION_NAME); state->capabilities.push_back(spv::CapabilityStencilExportEXT); state->spirvExtensions.push_back("SPV_EXT_shader_stencil_export"); + state->shaderStencilExport = true; } if (state->shaderClock) { deviceExtensions.push_back(VK_KHR_SHADER_CLOCK_EXTENSION_NAME); @@ -1310,9 +1338,11 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : } state->float16DenormPreserve = floatControls.shaderDenormPreserveFloat16 == VK_TRUE && floatControls.denormBehaviorIndependence == VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_ALL; - state->float32FlushToZeroCandidate = floatControls.shaderDenormFlushToZeroFloat32 == VK_TRUE && floatControls.denormBehaviorIndependence != VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_NONE && std::getenv("APS5_NO_HW_F32_FLUSH") == nullptr; - if (state->float32FlushToZeroCandidate) state->capabilities.push_back(spv::CapabilityDenormFlushToZero); - if (state->float16DenormPreserve && std::find(state->capabilities.begin(), state->capabilities.end(), static_cast(spv::CapabilityDenormPreserve)) == state->capabilities.end()) state->capabilities.push_back(spv::CapabilityDenormPreserve); + const auto float32Flush = Float32FlushControlFor(driverId, floatControls.shaderDenormFlushToZeroFloat32, floatControls.denormBehaviorIndependence); + state->float32FlushToZeroCandidate = float32Flush.probe && std::getenv("APS5_NO_HW_F32_FLUSH") == nullptr; + if (state->float32FlushToZeroCandidate && float32Flush.declared) state->capabilities.push_back(spv::CapabilityDenormFlushToZero); + state->float32DenormPreserve = floatControls.shaderDenormPreserveFloat32 == VK_TRUE && floatControls.denormBehaviorIndependence != VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_NONE && std::getenv("APS5_NO_HW_F32_PRESERVE") == nullptr; + if ((state->float16DenormPreserve || state->float32DenormPreserve) && std::find(state->capabilities.begin(), state->capabilities.end(), static_cast(spv::CapabilityDenormPreserve)) == state->capabilities.end()) state->capabilities.push_back(spv::CapabilityDenormPreserve); deviceExtensions.push_back(VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME); deviceExtensions.push_back(VK_KHR_8BIT_STORAGE_EXTENSION_NAME); state->capabilities.push_back(4448); @@ -1328,13 +1358,18 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->InstanceFunction("vkGetPhysicalDeviceProperties2")(selected, &properties); if (locations.sampleLocationSubPixelBits >= 4 && locations.sampleLocationCoordinateRange[0] <= 0.0f && locations.sampleLocationCoordinateRange[1] >= 15.0f / 16.0f) { const auto multisampleProperties = state->InstanceFunction("vkGetPhysicalDeviceMultisamplePropertiesEXT"); + std::string grids; for (const auto samples : {VK_SAMPLE_COUNT_1_BIT, VK_SAMPLE_COUNT_2_BIT, VK_SAMPLE_COUNT_4_BIT, VK_SAMPLE_COUNT_8_BIT}) { if ((locations.sampleLocationSampleCounts & samples) == 0) continue; VkMultisamplePropertiesEXT multisample{VK_STRUCTURE_TYPE_MULTISAMPLE_PROPERTIES_EXT}; multisampleProperties(selected, samples, &multisample); - if (multisample.maxSampleLocationGridSize.width != 0 && multisample.maxSampleLocationGridSize.height != 0) state->sampleLocationSampleCounts |= samples; - if (multisample.maxSampleLocationGridSize.width % 2u == 0 && multisample.maxSampleLocationGridSize.height % 2u == 0) state->sampleLocationGridSampleCounts |= samples; + const auto grid = multisample.maxSampleLocationGridSize; + state->sampleLocationGridSizes[static_cast(std::countr_zero(static_cast(samples)))] = grid; + if (grid.width != 0 && grid.height != 0) state->sampleLocationSampleCounts |= samples; + if (AgcDriver::Graphics::HostsSampleLocationGrid(grid)) state->sampleLocationGridSampleCounts |= samples; + grids += (grids.empty() ? "" : ", ") + std::to_string(static_cast(samples)) + " samples " + std::to_string(grid.width) + "x" + std::to_string(grid.height); } + if (!grids.empty()) std::fprintf(stderr, "[gpu] largest sample location grids (vkGetPhysicalDeviceMultisamplePropertiesEXT): %s; maxSampleLocationGridSize for every count %ux%u\n", grids.c_str(), locations.maxSampleLocationGridSize.width, locations.maxSampleLocationGridSize.height); } if (state->sampleLocationSampleCounts != 0) deviceExtensions.push_back(VK_EXT_SAMPLE_LOCATIONS_EXTENSION_NAME); } @@ -1485,7 +1520,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 +1529,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}; @@ -1502,6 +1539,15 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->pipelineLinkTimeOptimization = PipelineLinkTimeOptimizationUsable(driver.driverID); } } + VkPhysicalDeviceMaintenance5FeaturesKHR maintenance5Features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_5_FEATURES_KHR}; + if (state->graphicsPipelineLibrary && hasExtension(VK_KHR_MAINTENANCE_5_EXTENSION_NAME)) { + VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &maintenance5Features}; + state->InstanceFunction("vkGetPhysicalDeviceFeatures2")(selected, &features); + state->maintenance5 = maintenance5Features.maintenance5 == VK_TRUE; + if (state->maintenance5) deviceExtensions.push_back(VK_KHR_MAINTENANCE_5_EXTENSION_NAME); + } + maintenance5Features = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_5_FEATURES_KHR}; + maintenance5Features.maintenance5 = VK_TRUE; renderingFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_FEATURES_KHR, nullptr, VK_TRUE}; libraryFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_GRAPHICS_PIPELINE_LIBRARY_FEATURES_EXT, &dynamicStateFeatures, VK_TRUE}; dynamicStateFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTENDED_DYNAMIC_STATE_FEATURES_EXT, &renderingFeatures, VK_TRUE}; @@ -1631,6 +1677,8 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : // PA_CL_CLIP_CNTL near/far clip disable maps to depth clamping. enabled.depthClamp = available.depthClamp; state->depthClamp = enabled.depthClamp == VK_TRUE; + enabled.fillModeNonSolid = available.fillModeNonSolid; + state->fillModeNonSolid = enabled.fillModeNonSolid == VK_TRUE; enabled.depthBounds = available.depthBounds; state->depthBounds = enabled.depthBounds == VK_TRUE; enabled.depthBiasClamp = available.depthBiasClamp; @@ -1720,7 +1768,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; } @@ -1742,6 +1790,10 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : fifoLatestReadyFeatures.pNext = const_cast(deviceInfo.pNext); deviceInfo.pNext = &fifoLatestReadyFeatures; } + if (state->maintenance5) { + maintenance5Features.pNext = const_cast(deviceInfo.pNext); + deviceInfo.pNext = &maintenance5Features; + } if (state->maintenance8) { maintenance8Features.pNext = const_cast(deviceInfo.pNext); deviceInfo.pNext = &maintenance8Features; @@ -2100,6 +2152,10 @@ void VulkanDevice::WaitForPipelineJournalReplay() { if (state->journalReplay != nullptr) state->journalReplay->Wait(); } +void VulkanDevice::SavePipelineCache() { + if (state->pipelineCache) state->pipelineCache->SaveNow(); +} + std::uint64_t VulkanDevice::ComputePipelinesCreated() const { return state->computePipelinesCreated.load(std::memory_order_relaxed); } @@ -2223,6 +2279,10 @@ bool VulkanDevice::OpenWriteOverlaps(std::uint64_t address, std::size_t bytes) c return state->recorder && state->recorder->OpenWriteOverlaps(address, bytes); } +void VulkanDevice::SealPassWriteOnly() { + if (state->recorder) state->recorder->SealPassWriteOnly(); +} + bool VulkanDevice::RecordedWritesSettled(std::uint64_t address, std::size_t bytes) const { if (!state->recorder) return true; const auto& recorder = *state->recorder; @@ -2518,44 +2578,63 @@ std::atomic copiesAliased{0}, copyAliasCreated{0}, copyAliasNoSou // source range must be exactly a live image's surface, and the destination range either exactly // a live image of the same shape or none (one is made from the source's descriptor rebased). Under // GuestMemory::GpuMutex. Debug aid: APS5_NO_COPY_ALIAS=1 keeps every such copy a transfer. -bool AliasCopy(const Graphics::Context& context, std::uint64_t destination, std::uint64_t source, std::size_t bytes) { +bool CopyAliasDisabled() { static const bool disabled = std::getenv("APS5_NO_COPY_ALIAS") != nullptr; - if (disabled) return false; - auto from = Graphics::StorageTexture::FindLive(source, bytes); - if (from == nullptr) { + return disabled; +} + +struct AliasPlan { + std::shared_ptr from; + std::shared_ptr to; + const char* refusal = nullptr; +}; + +AliasPlan PlanAlias(const Graphics::Context& context, std::uint64_t destination, std::uint64_t source, std::size_t bytes) { + AliasPlan plan; + plan.from = Graphics::StorageTexture::FindLive(source, bytes); + if (plan.from == nullptr) { + plan.refusal = "no source"; + return plan; + } + plan.to = Graphics::StorageTexture::FindLive(destination, bytes); + plan.refusal = Graphics::StorageTexture::CopyRefusal(context, plan.to.get(), *plan.from, destination); + return plan; +} + +bool AliasCopy(const Graphics::Context& context, std::uint64_t destination, std::uint64_t source, std::size_t bytes) { + if (CopyAliasDisabled()) return false; + auto plan = PlanAlias(context, destination, source, bytes); + if (plan.from == nullptr) { copyAliasNoSource.fetch_add(1, std::memory_order_relaxed); return false; } - auto to = Graphics::StorageTexture::FindLive(destination, bytes); - if (to != nullptr && !to->SameSurfaceShape(*from)) { - copyAliasShape.fetch_add(1, std::memory_order_relaxed); + if (plan.refusal != nullptr) { + if (plan.to != nullptr && !plan.to->SameSurfaceShape(*plan.from)) { + copyAliasShape.fetch_add(1, std::memory_order_relaxed); + } else { + static std::atomic reported{0}; + if (reported.fetch_add(1) < 8) std::fprintf(stderr, "[copy] image copy 0x%llx -> 0x%llx (0x%zx bytes) refused: %s\n", static_cast(source), static_cast(destination), bytes, plan.refusal); + copyAliasRefused.fetch_add(1, std::memory_order_relaxed); + } return false; } - if (to == nullptr) { - auto resource = from->Descriptor(); + if (plan.to == nullptr) { + auto resource = plan.from->Descriptor(); resource.baseAddress = destination; resource.dccAddress = 0; - try { - to = Graphics::CachedStorageSurface(context, resource); - } catch (const std::exception& error) { - static std::atomic reported{0}; - if (reported.fetch_add(1) < 8) std::fprintf(stderr, "[copy] no storage image for the copy destination 0x%llx: %s\n", static_cast(destination), error.what()); - copyAliasRefused.fetch_add(1, std::memory_order_relaxed); - return false; - } + plan.to = Graphics::CachedStorageSurface(context, resource); copyAliasCreated.fetch_add(1, std::memory_order_relaxed); } const char* refusal = nullptr; - if (!to->CopyFrom(*from, refusal)) { - static std::atomic reported{0}; - if (reported.fetch_add(1) < 8) std::fprintf(stderr, "[copy] image copy 0x%llx -> 0x%llx (0x%zx bytes) refused: %s\n", static_cast(source), static_cast(destination), bytes, refusal); - copyAliasRefused.fetch_add(1, std::memory_order_relaxed); - return false; - } + if (!plan.to->CopyFrom(*plan.from, refusal)) throw std::runtime_error(std::string("AGC graphics: an image copy its check allowed was refused: ") + refusal); copiesAliased.fetch_add(1, std::memory_order_relaxed); return true; } +bool AliasCopyPossible(const Graphics::Context& context, std::uint64_t destination, std::uint64_t source, std::size_t bytes) { + return !CopyAliasDisabled() && PlanAlias(context, destination, source, bytes).refusal == nullptr; +} + } VulkanDevice::CopyVerification VulkanDevice::CopyVerifyCounts() { @@ -2575,21 +2654,34 @@ VulkanDevice::CopyAliasing VulkanDevice::CopyAliasCounts() { return {copiesAliased.load(std::memory_order_relaxed), copyAliasCreated.load(std::memory_order_relaxed), copyAliasNoSource.load(std::memory_order_relaxed), copyAliasShape.load(std::memory_order_relaxed), copyAliasRefused.load(std::memory_order_relaxed)}; } +bool VulkanDevice::completionStoresOver(std::uint64_t address, std::size_t bytes) const { + static const bool alwaysSync = std::getenv("APS5_COPY_SYNC") != nullptr; + if (alwaysSync || state->CopiedWriterOverlaps(address, bytes)) return true; + return Graphics::Recorder::PendingCompletionLabels() != 0 && state->recorder->CompletionLabelIn(address, bytes); +} + +bool VulkanDevice::copyWaitsOnCompletions(std::uint64_t address, std::size_t bytes) const { + return state->recorder->PendingWriteOverlaps(address, bytes) && completionStoresOver(address, bytes); +} + +bool VulkanDevice::GpuCopyPossible(std::uint64_t destination, std::uint64_t source, std::size_t bytes) const { + if (!state->recorder || bytes == 0 || copyWaitsOnCompletions(source, bytes) || copyWaitsOnCompletions(destination, bytes)) return false; + const auto context = graphicsContext(); + if (AliasCopyPossible(context, destination, source, bytes)) return true; + return Graphics::HostImportFor(context, destination, bytes) != nullptr && Graphics::HostImportFor(context, source, bytes) != nullptr; +} + VulkanDevice::CopyOutcome VulkanDevice::CopyBuffer(std::uint64_t destination, std::uint64_t source, std::size_t bytes, std::size_t cpuMax, std::size_t gpuMax, std::size_t knownMax, std::uint64_t programAddress, std::uint32_t queue, const CopyWriterNote& noteWriter) { CopyOutcome outcome{2, false, 0, CopyOutcome::None, false, false, 0, 0, 0, 0, false}; if (!state->recorder || bytes == 0) return outcome; auto& recorder = *state->recorder; // Finished batches were retired by the caller (Driver.cpp copyBuffer, on the graphics worker // only, as WriteLabelOnGpu reaps), so their writes neither refuse the CPU copy nor force a wait. - static const bool alwaysSync = std::getenv("APS5_COPY_SYNC") != nullptr; static const bool waitForSource = std::getenv("APS5_COPY_WAIT_SOURCE") != nullptr; static const bool verify = std::getenv("APS5_COPY_VERIFY") != nullptr; // A store a batch's completion makes on the CPU over the range (a copied buffer's write-back, // a completion-deferred label): it lands only when that batch is reaped, whatever its fence. - const auto completionStoreOverlaps = [&](std::uint64_t address) { - if (alwaysSync || state->CopiedWriterOverlaps(address, bytes)) return true; - return Graphics::Recorder::PendingCompletionLabels() != 0 && recorder.CompletionLabelIn(address, bytes); - }; + const auto completionStoreOverlaps = [&](std::uint64_t address) { return completionStoresOver(address, bytes); }; // The CPU decision (see the header): pure queries, in the order that makes the first failing // test the reason; a signaled writer of the source is fine (its in-place stores are final in // host memory) as long as no completion store or label of it is still to land. @@ -2667,8 +2759,8 @@ VulkanDevice::CopyOutcome VulkanDevice::CopyBuffer(std::uint64_t destination, st // before the transfer (or the device copy below) by its barrier; a store a batch's completion // makes on the CPU must land before the copy reads the source or writes the destination, so // the copy waits for that batch (SyncThrough) first. - const bool waitSource = recorder.PendingWriteOverlaps(source, bytes) && completionStoreOverlaps(source); - const bool waitDestination = recorder.PendingWriteOverlaps(destination, bytes) && completionStoreOverlaps(destination); + const bool waitSource = copyWaitsOnCompletions(source, bytes); + const bool waitDestination = copyWaitsOnCompletions(destination, bytes); if (waitSource || waitDestination) { outcome.synced = true; const auto syncStart = std::chrono::steady_clock::now(); @@ -3028,6 +3120,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; @@ -3079,10 +3178,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; @@ -3128,9 +3231,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; @@ -3141,6 +3242,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{}; @@ -3150,14 +3254,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"); @@ -3331,7 +3442,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; @@ -3399,6 +3510,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; @@ -3563,9 +3675,15 @@ ShaderRecompiler::SpirvTarget VulkanDevice::Target() const { require(state->shaderProfile != nullptr, "shader device profile is unavailable"); auto target = state->shaderProfile->Target(); target.float32FlushToZero = state->float32FlushToZero; + target.float32DenormPreserve = state->float32DenormPreserve; return target; } +Graphics::SampleOrder VulkanDevice::HostSampleOrder(const Graphics::State& graphics) const { + if (!graphics.sampleLocationGrid) return {}; + return Graphics::HostSampleLocationsFor(graphicsContext(), graphics).order; +} + ShaderRecompiler::SpirvTarget VulkanDevice::buildTarget() const { const auto& limits = state->properties.limits; ShaderRecompiler::SpirvTarget target{VK_API_VERSION_1_1, state->meshShader ? 0x00010400u : 0x00010300u, state->subgroup.subgroupSize, ShaderRecompiler::BdaAbi::Version, state->capabilities, state->spirvExtensions, false, {limits.maxComputeWorkGroupSize[0], limits.maxComputeWorkGroupSize[1], limits.maxComputeWorkGroupSize[2]}, limits.maxComputeWorkGroupInvocations, limits.maxComputeSharedMemorySize, {}, {}}; @@ -3579,6 +3697,7 @@ ShaderRecompiler::SpirvTarget VulkanDevice::buildTarget() const { target.fixedPushSlots = state->graphicsPipelineLibrary; target.registrationModules = state->graphicsPipelineLibrary; target.robustBufferAccess2 = state->robustBufferAccess2; + target.waveLdsMemoryBarriers = !state->subgroupControlBarriers; target.srgbDecodeFormats = state->srgbDecodeFormats & ~state->srgbCopyFormats; target.vertexSubgroups = (state->subgroup.supportedStages & VK_SHADER_STAGE_VERTEX_BIT) != 0; if (state->meshShader) { @@ -3711,6 +3830,8 @@ Graphics::Context VulkanDevice::buildContext() const { context.samplerTableUpdateAfterBind = SamplerTableUpdateAfterBind(context); context.geometryShader = state->geometryShader; context.sampleRateShading = state->sampleRateShading; + context.fillModeNonSolid = state->fillModeNonSolid; + context.maintenance5 = state->maintenance5; context.storageImageMultisample = state->storageImageMultisample; context.shaderClipDistance = state->shaderClipDistance; context.shaderCullDistance = state->shaderCullDistance; @@ -3728,7 +3849,9 @@ Graphics::Context VulkanDevice::buildContext() const { context.srgbCopyFormats = state->srgbCopyFormats; context.sampleLocationSampleCounts = state->sampleLocationSampleCounts; context.sampleLocationGridSampleCounts = state->sampleLocationGridSampleCounts; - context.memoryProperties2 = state->memoryBudget ? state->InstanceFunction("vkGetPhysicalDeviceMemoryProperties2") : nullptr; + context.sampleLocationGridSizes = state->sampleLocationGridSizes; + context.shaderStencilExport = state->shaderStencilExport; + context.memoryProperties2 =state->memoryBudget ? state->InstanceFunction("vkGetPhysicalDeviceMemoryProperties2") : nullptr; context.singlePassStorage = state->storageImageWithoutFormat; context.cacheReclaim = Graphics::ReleaseTextureCaches; return context; @@ -3799,6 +3922,10 @@ void VulkanDevice::Draw(const Graphics::State& graphics, const Pm4::DrawParamete if (trace) APS5_LOG_CHARS_OUT("VulkanDevice::Draw complete"); } +bool VulkanDevice::DrawInputsInPlace() const { + return state->drawInputsInPlace; +} + namespace { // The resource cache serves dispatches unless disabled; Revalidate proves a hit by texture identity, @@ -4276,6 +4403,8 @@ struct RecordedDispatch { std::shared_ptr bindings{}; std::uint64_t cpuArguments = 0; int cpuReason = 0; + std::array threadGroup{}; + std::array threadLimit{}; }; VkDevice VulkanDevice::Device() const { @@ -4532,8 +4661,8 @@ void VulkanDevice::Dispatch(const ShaderRecompiler::RecompileResult& shader, std static_cast(dispatch(shader, x, y, z, 0, snapshots, programAddress, std::move(prepared), recipe)); } -VulkanDevice::IndirectOutcome VulkanDevice::DispatchIndirect(const ShaderRecompiler::RecompileResult& shader, std::uint64_t arguments, std::span snapshots, std::uint64_t programAddress, std::shared_ptr prepared, std::shared_ptr* recipe) { - return dispatch(shader, 0, 0, 0, arguments, snapshots, programAddress, std::move(prepared), recipe); +VulkanDevice::IndirectOutcome VulkanDevice::DispatchIndirect(const ShaderRecompiler::RecompileResult& shader, std::uint64_t arguments, std::span snapshots, std::uint64_t programAddress, std::shared_ptr prepared, std::shared_ptr* recipe, const std::array& threadGroup) { + return dispatch(shader, 0, 0, 0, arguments, snapshots, programAddress, std::move(prepared), recipe, threadGroup); } void VulkanDevice::decideIndirect(RecordedDispatch& record, IndirectOutcome& outcome, char* groupsText) { @@ -4541,6 +4670,7 @@ void VulkanDevice::decideIndirect(RecordedDispatch& record, IndirectOutcome& out auto& timer = *record.timer; const auto& context = *record.context; const auto arguments = record.arguments; + const bool threadDimensions = record.threadGroup != std::array{}; // Decided here, after the resource build (which may retire imports and wait for recorded // work) and before the dispatch itself is recorded, so the import cannot be dropped before // the record. The build already recorded the copy-ins of its misaligned and staged regions @@ -4567,7 +4697,8 @@ void VulkanDevice::decideIndirect(RecordedDispatch& record, IndirectOutcome& out outcome.cpuReason = 8; } else if (labelPending() || state->CopiedWriterOverlaps(arguments, 12)) { outcome.cpuReason = 2; - } else if ((record.argumentImport = Graphics::HostImportFor(context, arguments, 12)) == nullptr) { + } else if ((record.argumentImport = Graphics::HostImportFor(context, arguments, 12)) == nullptr || (threadDimensions && record.argumentImport->address == 0)) { + record.argumentImport = nullptr; // Valid until the next refreshImports, which only runs under GuestMemory::GpuMutex (held // here, by every Dispatch caller) and Keeps a retired VkBuffer while the recorder is busy. outcome.cpuReason = 3; @@ -4584,6 +4715,10 @@ void VulkanDevice::decideIndirect(RecordedDispatch& record, IndirectOutcome& out Graphics::FinishLeaseWork(); std::array groups{}; GuestMemory::Read(arguments, std::as_writable_bytes(std::span(groups)), 4); + if (threadDimensions) { + record.threadLimit = groups; + for (std::size_t axis = 0; axis < groups.size(); ++axis) groups[axis] = groups[axis] / record.threadGroup[axis] + (groups[axis] % record.threadGroup[axis] != 0 ? 1u : 0u); + } outcome.argumentReadMs = std::chrono::duration(std::chrono::steady_clock::now() - readStart).count(); record.x = groups[0]; record.y = groups[1]; @@ -4660,6 +4795,24 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) { } recordStep(PhaseRecordDataRefresh); } + std::shared_ptr threadArguments; + if (record.threadGroup != std::array{}) { + const auto data = resources.shaderDataBuffer(*record.shader); + const auto limitDword = record.shader->program->imageMetadataDword; + Graphics::Require(data != VK_NULL_HANDLE && limitDword >= 3, "Vulkan dispatch: a thread-dimension dispatch has no ShaderData thread limit"); + const VkDeviceSize limitByte = static_cast(limitDword - 3) * sizeof(std::uint32_t); + Graphics::RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + if (argumentImport != nullptr) { + threadArguments = std::make_shared(context, 24, VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + Graphics::Require(recorder.RecordDispatchArguments(commands, argumentImport->address + (arguments - argumentImport->base), threadArguments->DeviceAddress(), &record.threadGroup), "Vulkan dispatch: thread-dimension indirect arguments need the argument kernel"); + const VkBufferCopy limit{12, limitByte, 12}; + context.Function("vkCmdCopyBuffer")(commands, threadArguments->Handle(), data, 1, &limit); + } else { + context.Resolved(&Graphics::DeviceFunctions::cmdUpdateBuffer, "vkCmdUpdateBuffer")(commands, data, limitByte, sizeof(record.threadLimit), record.threadLimit.data()); + } + resources.forgetShaderDataWords(*record.shader); + covered = 0; + } using CommandClass = Graphics::Recorder::CommandClass; if (Graphics::Recorder::BarrierValidate()) { if (argumentImport != nullptr) { @@ -4693,7 +4846,11 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) { } VkBuffer indirectBuffer = argumentImport != nullptr ? argumentImport->buffer : VK_NULL_HANDLE; VkDeviceSize indirectOffset = argumentImport != nullptr ? Graphics::HostImportOffset(*argumentImport, arguments) : 0; - if (argumentImport != nullptr && argumentImport->address != 0) { + if (threadArguments != nullptr) { + indirectBuffer = threadArguments->Handle(); + indirectOffset = 0; + recorder.Keep(std::move(threadArguments)); + } else if (argumentImport != nullptr && argumentImport->address != 0) { auto checked = std::make_shared(context, 12, VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT); if (recorder.RecordDispatchArguments(commands, argumentImport->address + (arguments - argumentImport->base), checked->DeviceAddress())) { indirectBuffer = checked->Handle(); @@ -4786,6 +4943,7 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) { // its bindings describe): what a dispatch or its preparation builds at the first use of a module, // and what the journal replay builds on its own threads before that use. std::shared_ptr VulkanDevice::createComputePipeline(const Graphics::Context& context, const ShaderRecompiler::RecompileResult& shader, VkDescriptorSetLayout setLayout, std::span layoutKey, VkShaderStageFlags pushStages, std::uint64_t programAddress) { + PerformanceTimer timing("Vulkan.ComputePipeline"); static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; const auto start = std::chrono::steady_clock::now(); auto objects = std::make_shared(); @@ -4793,10 +4951,7 @@ std::shared_ptr VulkanDevice::createComputePipeline(cons objects->destroyModule = state->DeviceFunction("vkDestroyShaderModule"); objects->destroyLayout = state->DeviceFunction("vkDestroyPipelineLayout"); objects->destroyPipeline = state->DeviceFunction("vkDestroyPipeline"); - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = shader.spirv.size() * sizeof(std::uint32_t); - moduleInfo.pCode = shader.spirv.data(); - check(state->DeviceFunction("vkCreateShaderModule")(state->device, &moduleInfo, nullptr, &objects->module), "vkCreateShaderModule"); + objects->module = Graphics::CreateShaderModule(state->DeviceFunction("vkCreateShaderModule"), state->device, state->graphicsPipelineLibrary, shader.spirv.data(), shader.spirv.size() * sizeof(std::uint32_t), "vkCreateShaderModule"); objects->resourcesLayout = setLayout; const std::array setLayouts{setLayout, shader.workgroupMemoryDwords != 0 ? state->workgroupMemoryLayout : VK_NULL_HANDLE}; const VkPushConstantRange push{VK_SHADER_STAGE_COMPUTE_BIT, 0, Graphics::ComputePushConstantBytes}; @@ -4856,11 +5011,7 @@ VkPipeline VulkanDevice::splitComputePipeline(const Graphics::Context& context, std::array offsets{}; for (std::uint32_t component = 0; component < offsets.size(); ++component) offsets[component] = {ShaderRecompiler::PipelineSpecialization::WorkgroupOffsetBase + component, base[component]}; const Graphics::PipelineSpecialization specialization(shader, offsets); - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = shader.spirv.size() * sizeof(std::uint32_t); - moduleInfo.pCode = shader.spirv.data(); - VkShaderModule module = VK_NULL_HANDLE; - check(state->DeviceFunction("vkCreateShaderModule")(state->device, &moduleInfo, nullptr, &module), "vkCreateShaderModule"); + VkShaderModule module = Graphics::CreateShaderModule(state->DeviceFunction("vkCreateShaderModule"), state->device, state->graphicsPipelineLibrary, shader.spirv.data(), shader.spirv.size() * sizeof(std::uint32_t), "vkCreateShaderModule"); VkPipeline pipeline = VK_NULL_HANDLE; try { CreateComputePipeline(state->device, state->DeviceFunction("vkCreateComputePipelines"), context.pipelineCache, module, objects.layout, computePipelineFlags(), computeStageFlags(shader), specialization.Info(), computeSubgroupSize(shader), &pipeline, context.pipelineCacheSynchronization); @@ -4911,8 +5062,19 @@ std::shared_ptr VulkanDevice::computePipeline(const Grap return finish(std::move(objects), nullptr); } -VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::span snapshots, std::uint64_t programAddress, std::shared_ptr prepared, std::shared_ptr* recipeOut) { +VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::span snapshots, std::uint64_t programAddress, std::shared_ptr prepared, std::shared_ptr* recipeOut, const std::array& threadGroup) { startJournalReplay(); + for (const auto& binding : shader.bindings) { + if (binding.role != ShaderRecompiler::DescriptorRole::GuestBuffers) continue; + for (std::size_t at = 0; at + 4u <= binding.guestDescriptor.size(); at += 4u) { + const auto* words = binding.guestDescriptor.data() + at; + const auto base = static_cast(words[0]) | (static_cast(words[1] & 0xffffu) << 32u); + const auto stride = (words[1] >> 16u) & 0x3fffu; + const auto records = static_cast(words[2]); + const auto bytes = stride != 0 ? records * stride : records; + if (base != 0 && bytes != 0) static_cast(Graphics::FlushDepthPlanes(base, static_cast(bytes))); + } + } state->recorder->BoundKeptBytes(); ShaderRecompiler::RuntimeAbi::RequireVersion(shader.runtimeAbiVersion); PerformanceTimer timing("Vulkan.Dispatch"); @@ -5049,6 +5211,10 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec Graphics::ShaderResources::MoveProof proof; auto moved = cached->MovedDispatchBuffers(shaders[0], state->recorder.get(), DispatchMoveBytes(), proof); moves.checked.fetch_add(1, std::memory_order_relaxed); + if (moved.has_value() && threadGroup != std::array{}) { + const auto data = cached->shaderDataAllocation(shaders[0]); + if (data.has_value() && std::ranges::any_of(*moved, [&](const Graphics::ShaderResources::MovedBuffer& entry) { return entry.allocation == *data; })) moved.reset(); + } if (moved.has_value() && !moved->empty()) { overlay = cached->PrepareDrawBindings(*state->recorder, *moved, true); if (overlay != nullptr && overlay->allocation.set == VK_NULL_HANDLE) overlay.reset(); @@ -5151,6 +5317,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec RecordedDispatch record{&context, &shaders[0], resources, objects, pushStages, &pushBytes, x, y, z, arguments, nullptr, programAddress, fromCache && TemplateDataRefresh() ? RecordedDispatch::DataRefresh::Words : RecordedDispatch::DataRefresh::None, 0, revalidateMs, &timer}; record.resourceKeyMatched = fromCache; record.bindings = overlay; + record.threadGroup = threadGroup; if (arguments != 0) decideIndirect(record, outcome, groupsText); // The indirect hold total below still covers the whole call of a CPU-resolved one. const bool indirect = record.arguments != 0 || outcome.cpuReason != 0; @@ -5225,7 +5392,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec return outcome; } -RecipeOutcome VulkanDevice::DispatchRecipe(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::uint64_t programAddress, const std::shared_ptr& hit, IndirectOutcome& outcome, const std::shared_ptr& verify, bool refreshByWords) { +RecipeOutcome VulkanDevice::DispatchRecipe(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::uint64_t programAddress, const std::shared_ptr& hit, IndirectOutcome& outcome, const std::shared_ptr& verify, bool refreshByWords, const std::array& threadGroup) { state->recorder->BoundKeptBytes(); ShaderRecompiler::RuntimeAbi::RequireVersion(shader.runtimeAbiVersion); PerformanceTimer timing("Vulkan.DispatchRecipe"); @@ -5321,6 +5488,7 @@ RecipeOutcome VulkanDevice::DispatchRecipe(const ShaderRecompiler::RecompileResu if (dataRefresh == RecordedDispatch::DataRefresh::Words) counters.refreshByWords.fetch_add(1, std::memory_order_relaxed); RecordedDispatch record{&context, &shaders[0], hit->resources, hit->objects, recipe.pushes ? VkShaderStageFlags{VK_SHADER_STAGE_COMPUTE_BIT} : VkShaderStageFlags{0}, &recipe.pushBytes, x, y, z, arguments, nullptr, programAddress, dataRefresh, recipe.dataWordsHash, 0, &timer}; record.resourceKeyMatched = verify != nullptr; + record.threadGroup = threadGroup; if (arguments != 0) decideIndirect(record, outcome, groupsText); const bool indirect = record.arguments != 0 || outcome.cpuReason != 0; const auto recordStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp index 8b3ad92f49..7bbca06017 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp @@ -27,6 +27,7 @@ namespace { std::jthread processSamplerThread; std::jthread workerSamplerThread; +std::jthread committerSamplerThread; // Debug aid: APS5_SAMPLE_WORKER= samples the calling thread every millisecond and, every 20 s, // writes "module+offset self inclusive" lines to for offline symbolization with nm. @@ -285,11 +286,7 @@ void StartProcessSampler() { }); } -} - -void StartWorkerSampler() { - StartProcessSampler(); - const char* path = std::getenv("APS5_SAMPLE_WORKER"); +void StartCallingThreadSampler(const char* path, std::jthread& slot) { if (path == nullptr) return; auto sampler = std::make_unique(); sampler->path = path; @@ -302,7 +299,7 @@ void StartWorkerSampler() { sampler->stackHigh = reinterpret_cast(block->StackBase); } if (!DuplicateHandle(GetCurrentProcess(), GetCurrentThread(), GetCurrentProcess(), &sampler->target, THREAD_ALL_ACCESS, FALSE, 0)) throw std::system_error(GetLastError(), std::system_category(), "Duplicating sampler thread handle"); - workerSamplerThread = std::jthread([sampler = std::move(sampler)](std::stop_token token) { + slot = std::jthread([sampler = std::move(sampler)](std::stop_token token) { auto flushed = std::chrono::steady_clock::now(); while (!token.stop_requested()) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); @@ -316,14 +313,28 @@ void StartWorkerSampler() { }); } +} + +void StartWorkerSampler() { + StartProcessSampler(); + StartCallingThreadSampler(std::getenv("APS5_SAMPLE_WORKER"), workerSamplerThread); +} + +void StartCommitterSampler() { + StartCallingThreadSampler(std::getenv("APS5_SAMPLE_COMMITTER"), committerSamplerThread); +} + void StopWorkerSampler() { processSamplerThread.request_stop(); workerSamplerThread.request_stop(); + committerSamplerThread.request_stop(); if (processSamplerThread.joinable()) processSamplerThread.join(); if (workerSamplerThread.joinable()) workerSamplerThread.join(); + if (committerSamplerThread.joinable()) committerSamplerThread.join(); } #else void StartWorkerSampler() {} +void StartCommitterSampler() {} void StopWorkerSampler() {} #endif diff --git a/core/libs/prx/libSceAgcDriver/Export.cpp b/core/libs/prx/libSceAgcDriver/Export.cpp new file mode 100644 index 0000000000..18cd1da5d8 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Export.cpp @@ -0,0 +1,183 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceAgcDriverAcquireRazorACQ(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverCreateQueue(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverCwsrResumeAcq(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverCwsrSuspendAcq(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverDebugHardwareStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverDestroyQueue(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverGetPaDebugInterfaceVersion(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverGetRegShadowInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverGetRegShadowInfoAgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverGetReservedDmemForAgc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverGetWorkloadStreamInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverIDHSSubmit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverModuleRegistration(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverNotifyDefaultStates(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverPassInfoDownward(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverPatchClearState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverSetFlip(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverSetHsOffchipParamDirect(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverSetWorkloadComplete(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverSetWorkloadsActive(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverSetupAsyncGraphics(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverSetupRegisterShadow(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverSubmitMultiCommandBuffersDirect(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverSubmitToHDRScopesACQ(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverSubmitToRazorACQ(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverSuspendPointSubmit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverSuspendPointSubmitDirect(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverSysEnableSubmitDone45Exception(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverSysGetClientNumber(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverSysIsGameClosed(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverTmpInitIdhs(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverUserDataWritePacket(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverUserDataWritePopMarker(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverUserDataWritePushMarker(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverUserDataWriteSetMarker(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp index 1c8ee60301..580236b120 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp @@ -25,6 +25,11 @@ public: std::vector messages; }; void SetImageTables(std::vector tables, std::vector> ranges); + struct SrtGuardFaults { + std::uint32_t stage = 0; + std::vector flattenedSrt; + }; + void SetSrtGuards(std::vector guards); // APS5_PROFILE_DRAW: page tables served from the per-device cache and built anew (cumulative), // and how many recent tables the cache holds right now. struct TableCacheStats { @@ -52,11 +57,13 @@ private: // same device addresses share one buffer while one of them is alive (see the table cache in // BdaResources.cpp, which refers to it weakly). void markWrittenPages() const; + void checkSrtGuard() const; std::shared_ptr table; std::unique_ptr fault; std::size_t tableBytes = 0; std::vector imageTables; std::vector> imageTableRanges; + std::vector srtGuards; }; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp index 5994cf4860..657c5351c4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp @@ -2,8 +2,10 @@ #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_BUFFERPOOL_HPP #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" +#include #include #include +#include #include #include #include @@ -24,6 +26,7 @@ struct BufferAllocation { VkDeviceSize offset = 0; bool slab = false; std::size_t bufferBytes = 0; + std::uint32_t memoryType = 0; }; struct SlabSlot { @@ -63,6 +66,10 @@ public: static std::size_t Capacity(std::size_t bytes, VkMemoryPropertyFlags properties); static VkBufferUsageFlags Usage(VkBufferUsageFlags usage, VkMemoryPropertyFlags properties); VkDeviceMemory AllocateMemory(const VkMemoryAllocateInfo& allocation, const char* operation, std::optional hostType = std::nullopt, std::uint32_t* allocatedType = nullptr); + static VkDeviceSize ImageBudget(VkDeviceSize deviceBudget); + VkDeviceMemory TakeImageMemory(const VkMemoryAllocateInfo& allocation, const char* operation, std::optional hostType = std::nullopt, std::uint32_t* allocatedType = nullptr); + void PutImageMemory(VkDeviceMemory memory, VkDeviceSize bytes, std::uint32_t memoryType) noexcept; + std::size_t RetainedImages(); std::optional Take(std::size_t bytes, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); void Put(const BufferAllocation& allocation) noexcept; static bool SlabEligible(std::size_t capacity, VkDeviceSize alignment, VkDeviceSize size, VkDeviceSize atom); @@ -82,6 +89,7 @@ private: }; struct Slab { std::vector available; + std::size_t blocks = 0; std::size_t emptyBlocks = 0; }; static std::uint64_t SlabKey(std::uint32_t memoryType, std::size_t capacity, bool addressable); @@ -89,16 +97,18 @@ private: struct Slot { BufferAllocation allocation; std::uint64_t lastUse; + std::list::iterator order; }; struct SlotKey { std::size_t bytes; VkBufferUsageFlags usage; VkMemoryPropertyFlags properties; + std::uint32_t memoryType = 0; bool operator==(const SlotKey&) const = default; }; struct SlotKeyHash { std::size_t operator()(const SlotKey& key) const noexcept { - return std::hash{}(key.bytes) ^ (static_cast(key.usage) << 32u) ^ (static_cast(key.properties) << 48u); + return std::hash{}(key.bytes) ^ (static_cast(key.usage) << 32u) ^ (static_cast(key.properties) << 48u) ^ (static_cast(key.memoryType) << 56u); } }; // One retention tier: its slots by key (each list oldest first), their count and bytes, the @@ -126,6 +136,9 @@ private: // caller destroys those after releasing the mutex, so builds taking buffers on other threads // do not wait behind the Vulkan destroy calls. Nothing changes when the vector cannot grow. void evictOldest(Tier& tier, std::vector& evicted); + bool evictOldestImage(std::vector& evicted); + bool imagePooled(VkDeviceSize bytes, std::uint32_t memoryType) const; + bool heapOverBudget(); // The retained-slot bound of each tier (APS5_BUFFER_POOL_SLOTS, default `defaultSlots`), read once. static std::size_t MaxSlots(); std::size_t maxSlots; @@ -136,11 +149,23 @@ private: PFN_vkFreeMemory freeMemory; PFN_vkAllocateMemory allocateMemory; PFN_vkMapMemory mapMemory; + VkPhysicalDevice physical = VK_NULL_HANDLE; + VkPhysicalDeviceMemoryProperties memoryProperties{}; + PFN_vkGetPhysicalDeviceMemoryProperties2 memoryProperties2 = nullptr; std::mutex mutex; // Not `small`/`large`: (via ) defines `small` as a macro. Tier smallTier; Tier largeTier; Tier deviceTier; + std::chrono::steady_clock::time_point heapRead{}; + bool heapOver = false; + VkDeviceSize imageBudget = 0; + VkDeviceSize imageBytes = 0; + std::size_t imageSlots = 0; + std::uint64_t imageHits = 0; + std::uint64_t imageMisses = 0; + std::uint64_t imageEvictions = 0; + std::list imageOrder; std::uint64_t clock = 0; std::mutex slabMutex; std::unordered_map slabs; @@ -156,7 +181,8 @@ private: }; std::shared_ptr GetBufferPool(const Context& context); -VkDeviceMemory AllocateImageMemory(const Context& context, const VkMemoryAllocateInfo& allocation, std::uint32_t mask, const char* operation); +VkDeviceMemory AllocateImageMemory(const Context& context, const VkMemoryAllocateInfo& allocation, std::uint32_t mask, const char* operation, std::uint32_t* allocatedType = nullptr); +void ReleaseImageMemory(const Context& context, VkDeviceMemory memory, VkDeviceSize bytes, std::uint32_t memoryType) noexcept; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index 4c13b4bea4..afaa005b1d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -6,6 +6,7 @@ #endif #include #include +#include #include #include #include @@ -20,6 +21,7 @@ namespace AgcDriver::Graphics { + class TextureDetiler; class GpuColorTransfer; class BufferPool; @@ -71,6 +73,17 @@ inline void Check(VkResult result, const char* operation) { if (result != VK_SUCCESS) throw std::runtime_error(std::string("AGC graphics: ") + operation + ": Vulkan result " + std::to_string(result)); } +inline VkShaderModule CreateShaderModule(PFN_vkCreateShaderModule create, VkDevice device, bool graphicsPipelineLibrary, const std::uint32_t* code, std::size_t bytes, const char* operation) { + std::vector keyed(code, code + bytes / sizeof(std::uint32_t)); + if (!graphicsPipelineLibrary && keyed.size() > 2) keyed[2] ^= 0x8000u; + VkShaderModuleCreateInfo info{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + info.codeSize = keyed.size() * sizeof(std::uint32_t); + info.pCode = keyed.data(); + VkShaderModule module = VK_NULL_HANDLE; + Check(create(device, &info, nullptr, &module), operation); + return module; +} + inline void Require(bool condition, const char* reason) { if (!condition) throw std::runtime_error(std::string("AGC graphics: ") + reason); } @@ -79,6 +92,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). @@ -220,6 +238,8 @@ struct Context { bool samplerTableUpdateAfterBind = false; bool geometryShader = false; bool sampleRateShading = false; + bool fillModeNonSolid = false; + bool maintenance5 = false; bool storageImageMultisample = false; bool shaderClipDistance = false; bool shaderCullDistance = false; @@ -241,6 +261,8 @@ struct Context { bool singlePassStorage = false; VkSampleCountFlags sampleLocationSampleCounts = 0; VkSampleCountFlags sampleLocationGridSampleCounts = 0; + std::array sampleLocationGridSizes{}; + bool shaderStencilExport = false; PipelineCacheSynchronization* pipelineCacheSynchronization = nullptr; bool shaderTerminateInvocation = false; @@ -332,6 +354,10 @@ struct Context { } }; +inline VkShaderModule CreateShaderModule(const Context& context, const std::uint32_t* code, std::size_t bytes, const char* operation) { + return CreateShaderModule(context.Function("vkCreateShaderModule"), context.device, context.graphicsPipelineLibrary, code, bytes, operation); +} + inline void EndRenderPass(const Context& context, VkCommandBuffer commands) { if (context.graphicsPipelineLibrary) context.Resolved(&DeviceFunctions::cmdEndRendering, "vkCmdEndRenderingKHR")(commands); else context.Resolved(&DeviceFunctions::cmdEndRenderPass, "vkCmdEndRenderPass")(commands); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp index 41e5198716..7f05d121a2 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp @@ -36,6 +36,7 @@ bool KeysServeSurface(std::uint64_t followedDcc, DccKeys uploaded, DccKeys fille // The keys covering a surface of `surfaceBytes`, when they all agree. DccKeys ReadDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes); bool IsDccClear(DccKeys keys); +std::optional LandedUniformMetadataByte(std::uint64_t address, std::size_t bytes); DccKeys CurrentDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes); DccKeys CurrentDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes, std::size_t keyCount); std::optional KnownPendingDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp index 8b2267cda4..521c60721b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp @@ -2,6 +2,7 @@ #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_DEPTHSURFACE_HPP #include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libSceAgcDriver/Graphics/include/SampleOrder.hpp" #include "prx/libSceAgcDriver/Graphics/include/State.hpp" #include #include @@ -9,13 +10,18 @@ #include #include #include +#include namespace AgcDriver::Graphics { class Texture; class StorageTexture; -VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target); +VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target, bool depthWritten = true); +SampleOrderTarget DepthSurfaceOrderTarget(const Context& context, const DepthTarget& target); +DepthSampleLocations DepthSurfaceSampleLocations(const Context& context, const DepthTarget& target); +std::vector> FlushDepthPlanes(std::uint64_t address, std::size_t bytes); +bool DepthPlanesStale(); bool SampleLocationsCompatibleDepth(const Context& context, VkFormat format, std::uint32_t samples); std::uint64_t DepthSurfacesSerial(); std::uint64_t DepthSurfacesReleased(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp index 3bece13c32..ceec04cf3e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp @@ -5,6 +5,7 @@ #include "BdaAbi.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include +#include #include #include #include @@ -46,6 +47,7 @@ struct HostImport { std::uint64_t pinnedBytes = 0; std::uint32_t memoryType = std::numeric_limits::max(); std::uint64_t backingSerial = 0; + mutable std::uint64_t spaceSerial = 0; }; inline VkDeviceSize HostImportOffset(const HostImport& import, std::uint64_t address) { @@ -72,15 +74,40 @@ ImportProbe ProbeDmaBufImportWriteProtection(const Context& context); ImportWatch PrepareImportWatch(const Context& context); void SetImportWatch(const Context& context, ImportWatch watch); +enum class ImportRequester : std::uint8_t { Map, Build, Access, Join, Segment }; +inline constexpr std::size_t ImportRequesterCount = 5; + +struct HostImportRequesterCounts { + std::uint64_t made = 0; + std::uint64_t madeBytes = 0; + std::uint64_t refused = 0; + std::uint64_t refusedBytes = 0; + std::uint64_t failed = 0; + std::uint64_t remembered = 0; +}; + +struct HostImportCounts { + std::uint64_t live = 0; + std::uint64_t liveBytes = 0; + std::array requesters{}; +}; + +const char* ImportRequesterName(ImportRequester requester); +HostImportCounts HostImportCounters(); +std::string FormatHostImportCounts(const HostImportCounts& counts); +void ReportHostImports(); + // The host import of the registered allocation containing [address, address + bytes), made on demand // (alignment and budget permitting), or null. Bytes at `address` are at `HostImportOffset(*import, // address)` in the import's buffer. -const HostImport* HostImportFor(const Context& context, std::uint64_t address, std::size_t bytes); +const HostImport* HostImportFor(const Context& context, std::uint64_t address, std::size_t bytes, ImportRequester requester = ImportRequester::Access); const HostImport* ExistingHostImportFor(const Context& context, std::uint64_t address, std::size_t bytes); void SetHostImportWindow(std::uint64_t bytes); // Tests: the window of one large registered range (64 MiB) and the import budget (0: the configured one). void SetHostImportRangeWindow(std::uint64_t bytes); void SetHostImportBudget(std::uint64_t bytes); +std::uint64_t HostImportBudgetFor(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t configured, std::uint64_t liveBytes); +std::uint64_t HostImportBudgetWith(std::uint64_t configured, bool set, std::uint64_t reported, bool allocationFailed, std::uint64_t ceiling); void SetAddressCopyLimit(std::uint64_t bytes); void SetHostImportDeferredSettles(bool enabled); // The larger of 6 GiB and half the physical memory, at most the largest host heap less the larger of 2 GiB and half @@ -219,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/GuestTextureResource.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp index fdd754bf3b..6f40129b56 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp @@ -2,6 +2,7 @@ #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_GUESTTEXTURERESOURCE_HPP #include "Recompiler.hpp" +#include #include #include @@ -96,6 +97,7 @@ float EffectiveMinLod(const GuestTextureResource& resource); bool IsNullTextureResource(std::span words); GuestTextureResource DecodeTextureResource(std::span words); bool DescriptorSingleLevel(std::span words); +bool FirstLayerDescriptor(std::span words, std::array& narrowed); bool MatchesGuestDimension(ShaderRecompiler::DescriptorImageShape shape, TextureDimension dimension); bool ArrayLayersServeView(const GuestTextureResource& surface, const GuestTextureResource& view); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp index 7f4ca5d9eb..c8a8800703 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp @@ -3,7 +3,9 @@ #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" +#include "prx/libSceAgcDriver/Graphics/include/SampleOrder.hpp" #include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include #include #include #include @@ -55,7 +57,15 @@ std::uint64_t MultisampledColorBytes(const ColorTarget& target); std::size_t MultisampledDccBytes(const ColorTarget& target); VkClearColorValue MultisampledDccClear(const ColorTarget& target, std::span dcc, std::span cmask); bool MultisampledFillClearColor(const ColorTarget& target, std::uint32_t fill, VkClearColorValue& clear); +struct MultisampledSingleClear { + std::uint64_t descriptor = 0; + std::array color{}; +}; +std::optional MatchMultisampledSingleClear(std::span code, std::span userData, const std::array& numThreads); +void NoteMultisampledSingleClear(const GuestTextureResource& resource, const std::array& color); +bool TakeMultisampledSingleClear(std::span code, std::span userData, const std::array& numThreads); bool TakeMultisampledClear(MultisampledCmask& cmask, MultisampledColorFill& colorFill, const ColorTarget& use, VkClearColorValue& clear); +SampleOrderTarget MultisampleTargetOrderTarget(const Context& context, const ColorTarget& target); std::shared_ptr MultisampleTargetTexture(const Context& context, const GuestTextureResource& resource, VkComponentMapping components, VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D); VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target, VkImage* image = nullptr); VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target, std::shared_ptr& owner, VkImage* image = nullptr); @@ -70,6 +80,7 @@ void ClearMultisampleTargets(VkDevice device); void InstallResidentImageReadGuard_nid_no_patch(); void UpdateResidentImageUnmapObserver_nid_no_patch(); void RequireNoMultisampleTarget(std::uint64_t address, std::size_t bytes, const char* access); +void RequireNoMultisampleColorTarget(std::uint64_t address, std::size_t bytes, const char* access); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/PassHazards.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/PassHazards.hpp index 9ce1cb70fc..298ca9c49e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/PassHazards.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/PassHazards.hpp @@ -22,6 +22,7 @@ struct PassAccess { std::span> attachments; bool anyReads = false; bool anyWrites = false; + GuestRangeList writeOnly = {}; }; enum class PassHazard : std::uint8_t { None, ReadAfterWrite, WriteAfterWrite, WriteAfterRead, Image, UnknownRead, UnknownWrite }; @@ -32,6 +33,7 @@ public: bool Overlaps(std::uint64_t begin, std::uint64_t end) const; bool Empty() const { return ranges.empty(); } std::size_t Size() const { return ranges.size(); } + GuestRangeList Ranges() const { return ranges; } void Clear() { ranges.clear(); } private: @@ -43,6 +45,7 @@ public: PassHazard Check(const PassAccess& access, bool samePass) const; void Add(const PassAccess& access); void BeginPass() { ++pass; } + void SealWriteOnly(); void Clear(); bool Empty() const; @@ -55,6 +58,7 @@ private: bool overlapsAny(const GuestRangeSet& set, GuestRangeList ranges) const; GuestRangeSet reads; GuestRangeSet writes; + GuestRangeSet writeOnly; std::vector> images; std::vector attachments; std::uint64_t pass = 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp index 61679c4f59..1ee631c131 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp @@ -58,6 +58,10 @@ public: void ContinueBackFaces(VkCommandBuffer commands, const State& state) const; bool SplitsSecondChannel() const { return secondChannel != VK_NULL_HANDLE; } void ContinueSecondChannel(VkCommandBuffer commands) const; + const SampleOrder& HostSampleOrder() const { return sampleOrder; } + bool ReordersSamples() const { return Graphics::ReordersSamples(sampleOrder); } + bool SampleLocationsEnabled() const { return sampleLocationsEnabled; } + VkExtent2D HostSampleLocationGrid() const { return sampleLocationGrid; } void PushConstants(VkCommandBuffer commands, VkShaderStageFlags stages, std::span bytes) const; // Forgets the Vulkan objects without destroying them: for entries of a device that is already gone. void Abandon() noexcept; @@ -93,6 +97,7 @@ private: std::vector colorSlots; bool sampleLocationsEnabled = false; VkExtent2D sampleLocationGrid{1, 1}; + SampleOrder sampleOrder{}; std::vector framebuffers; }; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp index bff387e234..4aaebda08a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp @@ -26,6 +26,7 @@ public: VkPipelineCache Handle() const { return cache; } PipelineCacheSynchronization& Synchronization() { return synchronization; } void SaveNow(); + void SaveUnlessBusy(); private: struct FileHeader { @@ -47,7 +48,7 @@ private: File readFile() const; std::optional readHeader() const; void load(std::vector& initialData); - void save(bool final); + void save(bool final, bool waitForPipelines); void run(); Context context; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp index f4d473d7a5..6ef4b5cf08 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp @@ -69,6 +69,7 @@ public: }; DrawPassStart StartDrawPass(std::uint64_t key, bool forced, bool fenced, const PassAccess& access); void LeaveRenderPassOpen(std::uint64_t key, std::uint32_t timing, bool fenced, const PassAccess& access, std::function afterPass = {}); + void SealPassWriteOnly(); // DCC "uncompressed" key stores (DccMetadata.cpp StoreUncompressedOnGpu): queued on the open // batch and recorded as one run (one barrier pair for every queued fill) at Submit, before a // label store (RecordStore), or before a command that writes or reads a queued range (the @@ -196,11 +197,12 @@ 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(); bool HasTimeline() const { return timeline != VK_NULL_HANDLE; } - bool RecordDispatchArguments(VkCommandBuffer commands, VkDeviceAddress source, VkDeviceAddress arguments); + bool RecordDispatchArguments(VkCommandBuffer commands, VkDeviceAddress source, VkDeviceAddress arguments, const std::array* threadGroup = nullptr); // Waits until every batch up to `serial` completed on the GPU. Called WITHOUT GuestMemory::GpuMutex: // it touches only the immutable timeline semaphore (the caller keeps the device alive). Requires // HasTimeline(); the debug switch APS5_NO_TIMELINE makes the device create the recorder without one. @@ -231,6 +233,7 @@ public: std::uint64_t end; bool computed; }; + static std::vector QueuedWriteOrder(std::span writes); static std::vector QueuedWriteGroups(std::span writes); // Waits only for the batches up to the newest one that writes the range (submitting the open // batch when it is that one); later batches stay in flight. Fences of one queue signal in @@ -804,6 +807,10 @@ private: Context context; VkSemaphore timeline = VK_NULL_HANDLE; + VkSemaphore submitHold = VK_NULL_HANDLE; + std::uint64_t submitHoldValue = 0; + struct ProgressWatcher; + std::unique_ptr progressWatcher; // 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/SampleOrder.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/SampleOrder.hpp new file mode 100644 index 0000000000..dc1a089fc5 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/SampleOrder.hpp @@ -0,0 +1,74 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_SAMPLEORDER_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_SAMPLEORDER_HPP + +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include +#include +#include +#include +#include +#include + +namespace AgcDriver::Graphics { + +struct HostSampleLocations { + VkExtent2D grid{1, 1}; + SampleOrder order{}; +}; + +enum class SampleOrderMethod : std::uint8_t { + Storage, + Depth, + DepthStencilExport, + DepthStencilBits +}; + +struct SampleOrderStep { + std::uint32_t attachment = 0; + SampleOrderMethod method = SampleOrderMethod::Storage; + bool toHost = false; + bool toGuest = false; +}; + +struct SampleOrderPass { + SampleOrder order{}; + VkExtent2D extent{}; + std::array locations{}; + bool located = false; +}; + +struct SampleOrderTarget { + VkImage image = VK_NULL_HANDLE; + VkImageView view = VK_NULL_HANDLE; + VkFormat format = VK_FORMAT_UNDEFINED; + VkExtent2D extent{}; + std::uint32_t samples = 1; + SampleOrderMethod method = SampleOrderMethod::Storage; + std::shared_ptr keep; +}; + +struct DepthSampleLocations { + VkImage image = VK_NULL_HANDLE; + VkFormat format = VK_FORMAT_UNDEFINED; + std::uint32_t samples = 1; + VkExtent2D grid{1, 1}; + std::array locations{}; + std::shared_ptr keep; +}; + +HostSampleLocations HostSampleLocationsFor(const Context& context, const State& state); +bool HostsSampleLocationGrid(VkExtent2D largestGrid); +bool MayReorderSamples(const Context& context, std::uint32_t samples); +SampleOrderMethod DepthSampleOrderMethod(const Context& context, const DepthTarget& depth); +VkFormat SampleOrderStorageFormat(const Context& context, VkFormat format, std::uint32_t elementBytes, std::uint32_t samples); +std::string SampleOrderRefusal(const Context& context, const State& state); +std::vector PlanSampleOrder(const Context& context, const State& state, const SampleOrder& order, VkAttachmentLoadOp load = VK_ATTACHMENT_LOAD_OP_LOAD, VkAttachmentStoreOp store = VK_ATTACHMENT_STORE_OP_STORE); +SampleOrderPass SampleOrderPassFor(const Context& context, const State& state, const SampleOrder& order); +void RecordSampleOrder(const Context& context, VkCommandBuffer commands, const SampleOrderPass& pass, std::span targets, bool toHost, std::vector>& kept); +void RecordDepthSampleLocations(const Context& context, VkCommandBuffer commands, const DepthSampleLocations& depth); +std::array, 9> SampleOrderShaderModules(); +void ClearSampleOrders(VkDevice device); + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 33918639b7..c0d65d1f74 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); @@ -72,7 +73,7 @@ struct TableDescriptors { std::uint32_t binding = 0; std::vector> runs; std::vector images; - std::shared_ptr>> textures; + std::shared_ptr>> textures; }; class DescriptorCache { @@ -233,6 +234,7 @@ public: // The recorder's hazard tracker inputs (Recorder::NoteAccess): the written elements' guest // ranges, the regions read in place, and the storage images with whether each is written. const std::vector>& GpuWrites() const { return guestMemory.Writes(); } + const std::vector>& WriteOnlyRanges() const { return writeOnlyRanges; } std::vector> InPlaceReads() const { return guestMemory.InPlaceReads(); } void InPlaceReads(std::vector>& out) const { guestMemory.InPlaceReads(out); } std::vector> StorageImages() const; @@ -275,6 +277,7 @@ public: static bool NeverReusable(std::span shaders); static std::uint64_t DepthGuardRefusals(); const std::vector>& SampledTextures() const { return textures; } + static std::size_t LookupLogCapacity(); bool ChecksFaults() const { return bda != nullptr; } void CheckFaults() const; // `shaders` are the stages the object was built from, in build order (a recorded draw's vertex @@ -304,7 +307,7 @@ public: bool DataWordsDiffer(const CompiledShader& shader) const; // Why the fast proof of a Revalidate left the object to the full walk (the [rescache] // revalidate line's reasons); Count: it did not. - enum class FastFail : std::size_t { NoRecord, Collect, Pending, Evicted, Changed, Keys, ClearedView, StorageKeys, Depth, Count }; + enum class FastFail : std::size_t { NoRecord, Collect, Pending, Evicted, Changed, Keys, ClearedView, StorageKeys, Depth, Replaced, Audit, Count }; // Why a Pending failure was left to the full walk instead of the own-object refresh (T1, see // refreshOwnObjects); Count: it was not. enum class OwnRefreshFallback : std::size_t { Disabled, Snapshot, Keys, ForeignView, SurfaceKey, NotImported, Uncached, Rerun, Count }; @@ -333,6 +336,9 @@ public: private: friend class AgcDriver::VulkanDevice; bool refreshData(VkCommandBuffer commands, const CompiledShader& shader, Recorder* recorder); + std::optional shaderDataAllocation(const CompiledShader& shader) const; + VkBuffer shaderDataBuffer(const CompiledShader& shader) const; + void forgetShaderDataWords(const CompiledShader& shader); struct DescribedRange { const char* kind; std::uint64_t address; @@ -342,6 +348,7 @@ private: std::uint32_t format = 0; int tileMode = 0; std::uint64_t dccAddress = 0; + bool imageTable = false; }; std::vector describedRanges; struct Allocation { @@ -352,6 +359,7 @@ private: ShaderRecompiler::DescriptorRole role = ShaderRecompiler::DescriptorRole::ShaderData; // Guest buffers: whether the shader may store to the range (see addGuestBuffer). bool written = true; + bool writeOnly = false; // Data buffers: the words the buffer holds once the recorded work ran (see RefreshData). std::vector dataWords; std::uint32_t adjustment = 0; @@ -538,6 +546,7 @@ private: GuestBufferMemory guestMemory; std::size_t maxDrawSnapshotBytes = MaxDrawSnapshotBytes(); std::unique_ptr bda; + std::vector srtGuardFaults; bool usesBda = false; bool bdaReadOnly = true; std::vector multisampleSources; @@ -569,7 +578,7 @@ private: std::vector> runs; }; std::array tableWrites; - std::shared_ptr keptTable(std::size_t set, std::shared_ptr>>& held) noexcept; + std::shared_ptr keptTable(std::size_t set, std::shared_ptr>>& held) noexcept; std::uint32_t setOf(const Binding& binding) const; std::vector allocations; std::vector descriptorBuffers; @@ -587,6 +596,8 @@ private: // Guest buffer elements bound read-only: each use of this object skips that many pending-write // notes (counted in MarkGpuWrites for the [buffers] line). std::size_t readOnlyBuffers = 0; + std::chrono::steady_clock::time_point lastFullWalk = std::chrono::steady_clock::now(); + std::vector> writeOnlyRanges; bool drawBuild = false; std::uint64_t dispatchThreads = 0; std::vector> textures; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index 95fc4ae25f..d1dcf02e26 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -143,6 +143,7 @@ struct State { VkConservativeRasterizationModeEXT conservativeRasterization = VK_CONSERVATIVE_RASTERIZATION_MODE_DISABLED_EXT; VkRect2D scissor; VkCullModeFlags cullMode; + VkPolygonMode polygonMode = VK_POLYGON_MODE_FILL; VkFrontFace frontFace; VkProvokingVertexModeEXT provokingVertexMode = VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT; VkPipelineColorBlendAttachmentState blend; @@ -167,6 +168,14 @@ void NormalizeUnusedViewportDepth(State& state, const ShaderRecompiler::ShaderPi std::array SampleLocations(const State& state); std::array PixelSampleLocations(const State& state, const ShaderRecompiler::ShaderPixelStageInfo& pixel); VkExtent2D SampleLocationGrid(const State& state); +using SampleOrder = std::array; +inline constexpr std::uint32_t KeptSampleOrder = 0x76543210u; +std::optional SameSetSampleOrder(const State& state); +bool ReordersSamples(const SampleOrder& order); +std::uint32_t InvertSampleOrder(std::uint32_t order); +std::uint32_t HostSampleBits(std::uint32_t guestBits, std::uint32_t order); +bool SampleMaskFollowsOrder(const State& state, const SampleOrder& order); +std::array QuadSampleMasks(const State& state, const SampleOrder& order); std::array ExportMappings(const State& state); std::array ExportPackings(const State& state); ShaderRecompiler::RasterPrimitive RasterPrimitiveOf(const State& state); @@ -270,6 +279,7 @@ inline constexpr std::array DrawKeyRegisters{{ {RegisterBank::UserConfig, 0x242, 1}, {RegisterBank::UserConfig, 0x24b, 1}, {RegisterBank::UserConfig, 0x25b, 1}, }}; bool SplitsAlternateAlpha(const State& state); +bool StencilOperationValueReplaces(const Registers& context); bool StencilReferenceUnused(const State& state); // Whether DrawKeyRegisters holds the read. bool DrawKeyCovers(RegisterRead read); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index b271f40672..ae4f6dcaec 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -46,13 +46,13 @@ struct OwnedImage { OwnedImage(const Context& context, VkImage image, VkDeviceMemory memory) : context(context), image(image), memory(memory) {} OwnedImage(const OwnedImage&) = delete; OwnedImage& operator=(const OwnedImage&) = delete; - ~OwnedImage() { - if (image) context.Function("vkDestroyImage")(context.device, image, nullptr); - if (memory) context.Function("vkFreeMemory")(context.device, memory, nullptr); - } + ~OwnedImage(); + void Allocate(const Context& device, const VkMemoryAllocateInfo& allocation, std::uint32_t mask, const char* operation); Context context; VkImage image; VkDeviceMemory memory; + VkDeviceSize memoryBytes = 0; + std::uint32_t memoryType = 0; }; class Texture { @@ -63,7 +63,7 @@ public: // compute pass writing it and the next pass sampling it share one image and copy nothing. // CanCopyFrom says whether the two descriptors address the same surface compatibly. Texture(const Context& context, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components); - Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components, VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D, std::uint32_t baseLayer = 0, std::shared_ptr owner = {}); + Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components, VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D, std::uint32_t baseLayer = 0, std::shared_ptr owner = {}, VkImageUsageFlags usage = 0); struct DepthCopyTag {}; Texture(const Context& context, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components, DepthCopyTag); struct SrgbExpandTag {}; @@ -197,7 +197,8 @@ public: void WriteBack(); // Deferred write-back (APS5_EAGER_WRITEBACK=1 stores at once instead). void MarkDirty(); - void MarkSliceDirty(std::uint32_t slice); + void MarkWholeWritten(); + void MarkSliceDirty(std::uint32_t slice, std::uint32_t count = 1); void Flush(); bool Dirty() const; // Stores every pending image overlapping the range, except `except`; returns whether any was. @@ -209,6 +210,7 @@ public: // overlapping pending image, as before). static bool FlushPending(std::uint64_t address, std::size_t bytes, const StorageTexture* except = nullptr, const char* reason = "memory access", PublishScope scope = PublishScope::Whole, bool* published = nullptr); static void FlushAllPending(const char* reason); + static bool WaitForFlushing(std::uint64_t address, std::size_t bytes, const StorageTexture* except = nullptr); static void SaveBeforeUnwatch(std::uint64_t address, std::size_t bytes); static constexpr std::uint64_t SmallStoreBytes = 64 * 1024; static std::size_t StoreSmallPending(const char* reason); @@ -338,6 +340,7 @@ public: // may carry DCC metadata, and this surface must be host-imported (the write-back is GPU-direct). // False, naming why, when the copy must be a transfer. bool CopyFrom(StorageTexture& source, const char*& refusal); + static const char* CopyRefusal(const Context& context, const StorageTexture* destination, const StorageTexture& source, std::uint64_t address); VkImage Image() const { return image; } const GuestTextureResource& Descriptor() const { return descriptor; } std::uint32_t ImageLayers() const { return geometry.imageLayers; } @@ -364,6 +367,9 @@ public: static std::uint64_t SinglePassMoves(); static std::uint64_t SlabRetiles(); static std::uint64_t RefreshesProved(); + static std::uint64_t ClearHandOvers(); + static std::uint64_t HandedOverSuperseded(); + static void SetClearHandOver(bool enabled); static std::uint64_t UntrackedCompares(); std::uint64_t GuestBytes() const; std::uint64_t BudgetBytes() const; @@ -503,6 +509,8 @@ private: // stays responsible for storing them until this image's own results supersede them // (markLayersPending). APS5_NO_ALIAS_BORROW=1 stores and re-reads as before. std::shared_ptr pendingAlias() const; + std::optional clearHandOver(); + void supersedeHandedOver(std::uint32_t first, std::uint32_t count); // The pending images FlushPending(address, bytes) would store (not the one a Refresh validates, // a pending unit inside the range), kept alive across the caller's use. static std::vector> overlappingPending(std::uint64_t address, std::size_t bytes); @@ -583,6 +591,8 @@ private: std::weak_ptr borrowedFrom; std::uint64_t borrowedVersion = 0; std::vector borrowedUnits; + std::vector, std::uint64_t>> handedOver; + std::vector handedUnits; // Units of this image were borrowed since it was last written: the borrower's proofs // (the resource cache's serial memo) hold only while this image's version does, so the next // write here bumps the pending serial although the image was pending already. @@ -590,6 +600,7 @@ private: VkImage image = VK_NULL_HANDLE; VkDeviceMemory memory = VK_NULL_HANDLE; VkDeviceSize memoryBytes = 0; + std::uint32_t memoryType = 0; VkImageView view = VK_NULL_HANDLE; std::uint32_t defaultMip = 0; std::map extraViews; @@ -608,6 +619,7 @@ private: // Results are on the GPU only (guarded by the pending-write registry lock). bool dirty = false; std::uint64_t version = 0; + std::uint64_t writes = 0; // `original` holds the guest bytes; false after a GPU-side clear, which never read them. std::vector layerOriginal; std::map keptAtUnwatch; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp index 9271395ec1..a2625c2e6b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp @@ -19,6 +19,7 @@ VkFormat SampledTextureFormat(const Context& context, std::uint32_t guestFormat) std::uint32_t SrgbCopyFormats(PFN_vkGetPhysicalDeviceFormatProperties formatProperties, VkPhysicalDevice physical, std::uint32_t decodeFormats); bool SrgbExpandsToRgba8(const Context& context, std::uint32_t guestFormat); bool IsSrgbTextureFormat(std::uint32_t guestFormat); +bool IgnoresForcedDegamma(std::uint32_t guestFormat); std::optional FindGuestTextureFormat(VkFormat format, std::uint32_t elementBytes); std::optional FindGuestColorTargetFormat(VkFormat format, std::uint32_t elementBytes); std::uint32_t BytesPerElement(std::uint32_t guestFormat); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp index b83d4f4f89..5699bf78e1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp @@ -75,6 +75,8 @@ struct SurfaceGeometry { }; SurfaceGeometry DescribeSurface(const GuestTextureResource& descriptor); bool LevelsFitAllocation(const GuestTextureResource& surface, std::uint32_t levels); +GuestTextureResource ViewedArrayWindow(const GuestTextureResource& viewed); +GuestTextureResource ArrayLayerWindow(const GuestTextureResource& viewed, std::uint32_t maxArrayLayers); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp index 5c71753057..a8d93a9a85 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp @@ -363,23 +363,36 @@ inline std::optional ClampDrawIndices(std::span indice return highest; } +template +std::optional HighestDrawIndexOf(std::span indices, bool skipRestart) { + constexpr auto restartIndex = std::numeric_limits::max(); + const auto count = indices.size() / sizeof(TIndex); + const auto* data = indices.data(); + TIndex highest = 0; + if (!skipRestart) { + for (std::size_t i = 0; i < count; ++i) { + TIndex index; + std::memcpy(&index, data + i * sizeof(TIndex), sizeof(TIndex)); + highest = std::max(highest, index); + } + if (count == 0) return std::nullopt; + return highest; + } + TIndex kept = 0; + for (std::size_t i = 0; i < count; ++i) { + TIndex index; + std::memcpy(&index, data + i * sizeof(TIndex), sizeof(TIndex)); + const TIndex valid = index == restartIndex ? TIndex{0} : TIndex(~TIndex{0}); + highest = std::max(highest, static_cast(index & valid)); + kept |= valid; + } + if (kept == 0) return std::nullopt; + return highest; +} + inline std::optional HighestDrawIndex(std::span indices, std::uint32_t indexSize, bool skipRestart) { Require(indexSize == 2 || indexSize == 4, "unsupported index size"); - const auto restartIndex = indexSize == 2 ? 0xffffu : 0xffffffffu; - std::optional highest; - for (std::size_t offset = 0; offset + indexSize <= indices.size(); offset += indexSize) { - std::uint32_t index = 0; - if (indexSize == 2) { - std::uint16_t value = 0; - std::memcpy(&value, indices.data() + offset, sizeof(value)); - index = value; - } else { - std::memcpy(&index, indices.data() + offset, sizeof(index)); - } - if (skipRestart && index == restartIndex) continue; - highest = std::max(highest.value_or(0u), index); - } - return highest; + return indexSize == 2 ? HighestDrawIndexOf(indices, skipRestart) : HighestDrawIndexOf(indices, skipRestart); } inline std::vector MeshRestartTable(std::span indices, std::uint32_t indexSize) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/DispatchArguments.comp b/core/libs/prx/libSceAgcDriver/Graphics/shaders/DispatchArguments.comp index ed0b252561..2ebd51c40e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/shaders/DispatchArguments.comp +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/DispatchArguments.comp @@ -9,13 +9,29 @@ layout(push_constant) uniform Parameters { uint maxX; uint maxY; uint maxZ; + uint threadDimensions; + uint groupX; + uint groupY; + uint groupZ; uint unused; } parameters; +uint groups(uint threads, uint group) { + return threads / group + (threads % group != 0u ? 1u : 0u); +} + void main() { uint x = parameters.source.words[0]; uint y = parameters.source.words[1]; uint z = parameters.source.words[2]; + if (parameters.threadDimensions != 0u) { + parameters.arguments.words[3] = x; + parameters.arguments.words[4] = y; + parameters.arguments.words[5] = z; + x = groups(x, parameters.groupX); + y = groups(y, parameters.groupY); + z = groups(z, parameters.groupZ); + } bool valid = x <= parameters.maxX && y <= parameters.maxY && z <= parameters.maxZ; parameters.arguments.words[0] = valid ? x : 0u; parameters.arguments.words[1] = valid ? y : 0u; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/SampleOrder.comp b/core/libs/prx/libSceAgcDriver/Graphics/shaders/SampleOrder.comp new file mode 100644 index 0000000000..f8a2323e90 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/SampleOrder.comp @@ -0,0 +1,60 @@ +#version 450 + +layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in; + +layout(binding = 0, TEXEL_FORMAT) uniform restrict uimage2DMS samples; + +layout(push_constant) uniform Order { + uvec4 map; + uvec2 extent; + uint count; +} order; + +uvec4 pick(uint source, uvec4 s0, uvec4 s1, uvec4 s2, uvec4 s3, uvec4 s4, uvec4 s5, uvec4 s6, uvec4 s7) { + const bool odd = (source & 1u) != 0u; + const uvec4 low = (source & 2u) == 0u ? (odd ? s1 : s0) : (odd ? s3 : s2); + const uvec4 high = (source & 2u) == 0u ? (odd ? s5 : s4) : (odd ? s7 : s6); + return (source & 4u) == 0u ? low : high; +} + +void reorder(ivec2 at, uint map) { + if (map == 0x76543210u || uint(at.x) >= order.extent.x || uint(at.y) >= order.extent.y) return; + const uvec4 s0 = imageLoad(samples, at, 0); + const uvec4 s1 = imageLoad(samples, at, 1); + uvec4 s2 = uvec4(0u); + uvec4 s3 = uvec4(0u); + uvec4 s4 = uvec4(0u); + uvec4 s5 = uvec4(0u); + uvec4 s6 = uvec4(0u); + uvec4 s7 = uvec4(0u); + if (order.count > 2u) { + s2 = imageLoad(samples, at, 2); + s3 = imageLoad(samples, at, 3); + } + if (order.count > 4u) { + s4 = imageLoad(samples, at, 4); + s5 = imageLoad(samples, at, 5); + s6 = imageLoad(samples, at, 6); + s7 = imageLoad(samples, at, 7); + } + imageStore(samples, at, 0, pick(map & 7u, s0, s1, s2, s3, s4, s5, s6, s7)); + imageStore(samples, at, 1, pick((map >> 4u) & 7u, s0, s1, s2, s3, s4, s5, s6, s7)); + if (order.count > 2u) { + imageStore(samples, at, 2, pick((map >> 8u) & 7u, s0, s1, s2, s3, s4, s5, s6, s7)); + imageStore(samples, at, 3, pick((map >> 12u) & 7u, s0, s1, s2, s3, s4, s5, s6, s7)); + } + if (order.count > 4u) { + imageStore(samples, at, 4, pick((map >> 16u) & 7u, s0, s1, s2, s3, s4, s5, s6, s7)); + imageStore(samples, at, 5, pick((map >> 20u) & 7u, s0, s1, s2, s3, s4, s5, s6, s7)); + imageStore(samples, at, 6, pick((map >> 24u) & 7u, s0, s1, s2, s3, s4, s5, s6, s7)); + imageStore(samples, at, 7, pick((map >> 28u) & 7u, s0, s1, s2, s3, s4, s5, s6, s7)); + } +} + +void main() { + const ivec2 quad = ivec2(gl_GlobalInvocationID.xy) * 2; + reorder(quad, order.map.x); + reorder(quad + ivec2(1, 0), order.map.y); + reorder(quad + ivec2(0, 1), order.map.z); + reorder(quad + ivec2(1, 1), order.map.w); +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/SampleOrder.frag b/core/libs/prx/libSceAgcDriver/Graphics/shaders/SampleOrder.frag new file mode 100644 index 0000000000..89e5983334 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/SampleOrder.frag @@ -0,0 +1,31 @@ +#version 450 +#extension GL_EXT_samplerless_texture_functions : require +#ifdef STENCIL_EXPORT +#extension GL_ARB_shader_stencil_export : require +#endif + +layout(binding = 0) uniform texture2DMS depthSource; +layout(binding = 1) uniform utexture2DMS stencilSource; + +layout(push_constant) uniform Order { + uvec4 map; + uint bit; +} order; + +void main() { + const ivec2 at = ivec2(gl_FragCoord.xy); + const bool right = (at.x & 1) != 0; + const uint top = right ? order.map.y : order.map.x; + const uint bottom = right ? order.map.w : order.map.z; + const uint map = (at.y & 1) != 0 ? bottom : top; + if (map == 0x76543210u) discard; + const int source = int((map >> (uint(gl_SampleID) * 4u)) & 7u); +#ifdef STENCIL_BITS + if (((texelFetch(stencilSource, at, source).r >> order.bit) & 1u) == 0u) discard; +#else + gl_FragDepth = texelFetch(depthSource, at, source).r; +#ifdef STENCIL_EXPORT + gl_FragStencilRefARB = int(texelFetch(stencilSource, at, source).r); +#endif +#endif +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/SampleOrder.vert b/core/libs/prx/libSceAgcDriver/Graphics/shaders/SampleOrder.vert new file mode 100644 index 0000000000..05606ecec4 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/SampleOrder.vert @@ -0,0 +1,6 @@ +#version 450 + +void main() { + const vec2 corner = vec2(float((gl_VertexIndex << 1) & 2), float(gl_VertexIndex & 2)); + gl_Position = vec4(corner * 2.0 - 1.0, 0.0, 1.0); +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp index e6365c0aca..1da043f87f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp @@ -127,6 +127,7 @@ BdaResources::BdaResources(const Context& context) { Require(ShaderRecompiler::BdaAbi::FaultBufferBytes <= context.limits.maxStorageBufferRange, "BDA fault buffer exceeds storage buffer range limit"); fault = std::make_unique(context, ShaderRecompiler::BdaAbi::FaultBufferBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); std::memset(fault->Bytes().data(), 0, ShaderRecompiler::BdaAbi::WrittenListWord * sizeof(std::uint32_t)); + std::memset(fault->Bytes().data() + ShaderRecompiler::BdaAbi::SrtGuardWord * sizeof(std::uint32_t), 0, sizeof(std::uint32_t)); } BdaResources::BdaResources(const Context& context, const GuestBufferMemory& memory) : BdaResources(context) { @@ -244,10 +245,11 @@ 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"); + checkSrtGuard(); 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 +295,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()); } @@ -305,6 +311,27 @@ void BdaResources::SetImageTables(std::vector tables, std::vec imageTableRanges = std::move(ranges); } +void BdaResources::SetSrtGuards(std::vector guards) { + srtGuards = std::move(guards); +} + +void BdaResources::checkSrtGuard() const { + namespace Abi = ShaderRecompiler::BdaAbi; + std::uint32_t guard = 0; + std::memcpy(&guard, fault->Bytes().data() + Abi::SrtGuardWord * sizeof(std::uint32_t), sizeof(guard)); + if (guard == 0) return; + const auto stage = guard >> Abi::SrtGuardStageShift; + const auto record = guard & ((1u << Abi::SrtGuardStageShift) - 1u); + const auto found = std::find_if(srtGuards.begin(), srtGuards.end(), [&](const SrtGuardFaults& candidate) { return candidate.stage == stage; }); + const auto* guards = found != srtGuards.end() ? &*found : srtGuards.size() == 1 ? &srtGuards.front() : nullptr; + Require(guards != nullptr && record + 2u < guards->flattenedSrt.size(), "a guarded SRT read faulted without its poison record"); + const auto& words = guards->flattenedSrt; + const auto address = static_cast(words[record + 1u]) | (static_cast(words[record + 2u]) << 32u); + std::ostringstream message; + message << "BDA access failed: address=0x" << std::hex << address << " instruction=0x" << words[record] << std::dec << " bytes=4 stage=" << stage << " reason=" << static_cast(Abi::FaultReason::Unmapped); + throw std::runtime_error(message.str()); +} + void BdaResources::markWrittenPages() const { namespace Abi = ShaderRecompiler::BdaAbi; auto* words = reinterpret_cast(fault->Bytes().data()); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp index e2f7fc6dc7..128970a898 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include namespace AgcDriver::Graphics { @@ -37,10 +38,14 @@ constexpr VkBufferUsageFlags deviceUsage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | } -BufferPool::BufferPool(const Context& context) : device(context.device), unmap(context.Function("vkUnmapMemory")), destroyBuffer(context.Function("vkDestroyBuffer")), freeMemory(context.Function("vkFreeMemory")), allocateMemory(context.Function("vkAllocateMemory")), mapMemory(context.Function("vkMapMemory")), maxSlots(std::max(1, std::min(MaxSlots(), context.limits.maxMemoryAllocationCount / 6u))), cacheReclaim(context.cacheReclaim) { +BufferPool::BufferPool(const Context& context) : maxSlots(std::max(1, std::min(MaxSlots(), context.limits.maxMemoryAllocationCount / 6u))), cacheReclaim(context.cacheReclaim), device(context.device), unmap(context.Function("vkUnmapMemory")), destroyBuffer(context.Function("vkDestroyBuffer")), freeMemory(context.Function("vkFreeMemory")), allocateMemory(context.Function("vkAllocateMemory")), mapMemory(context.Function("vkMapMemory")), physical(context.physical), memoryProperties(context.memory), memoryProperties2(context.memoryProperties2) { smallTier.budget = smallBudget; largeTier.budget = budget; deviceTier.budget = deviceBudget(context); + memoryProperties = context.memory; + physical = context.physical; + memoryProperties2 = context.memoryProperties2; + imageBudget = ImageBudget(deviceTier.budget); } BufferPool::~BufferPool() { @@ -60,7 +65,7 @@ VkDeviceMemory BufferPool::AllocateMemory(const VkMemoryAllocateInfo& allocation unsigned attempts = 1; VkDeviceSize reclaimed = 0; VkDeviceSize released = 0; - const auto exhausted = [](VkResult value) { return value == VK_ERROR_OUT_OF_DEVICE_MEMORY || value == VK_ERROR_OUT_OF_HOST_MEMORY; }; + const auto exhausted = [](VkResult value) { return value == VK_ERROR_OUT_OF_DEVICE_MEMORY || value == VK_ERROR_OUT_OF_HOST_MEMORY || value == VK_ERROR_TOO_MANY_OBJECTS; }; if (exhausted(result)) { reclaimed = trim(); if (reclaimed != 0) { @@ -72,6 +77,7 @@ VkDeviceMemory BufferPool::AllocateMemory(const VkMemoryAllocateInfo& allocation if (exhausted(result)) { if (cacheReclaim != nullptr) released = cacheReclaim(ReclaimTarget(allocation.allocationSize)); if (released != 0) { + reclaimed += trim(); memory = VK_NULL_HANDLE; result = allocateMemory(device, &allocation, nullptr, &memory); ++attempts; @@ -96,12 +102,120 @@ VkDeviceMemory BufferPool::AllocateMemory(const VkMemoryAllocateInfo& allocation if (attempts > 1) std::fprintf(stderr, "[bufferpool] %s recovered: allocation=%llu reclaimed=%llu released=%llu initialResult=%d attempts=%u\n", operation, static_cast(allocation.allocationSize), static_cast(reclaimed), static_cast(released), static_cast(initialResult), attempts); return memory; } - char details[512]; + char details[1536]; std::snprintf(details, sizeof(details), "%s: allocation=%llu memoryType=%u reclaimed=%llu released=%llu initialResult=%d attempts=%u", operation, static_cast(allocation.allocationSize), allocation.memoryTypeIndex, static_cast(reclaimed), static_cast(released), static_cast(initialResult), attempts); + if (memoryProperties2 != nullptr && physical != VK_NULL_HANDLE) { + VkPhysicalDeviceMemoryBudgetPropertiesEXT budget{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT}; + VkPhysicalDeviceMemoryProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2, &budget}; + memoryProperties2(physical, &properties); + auto used = std::strlen(details); + for (std::uint32_t heap = 0; heap < memoryProperties.memoryHeapCount && heap < VK_MAX_MEMORY_HEAPS; ++heap) { + const auto remaining = sizeof(details) - used; + const auto appended = std::snprintf(details + used, remaining, "; heap %u %llu/%llu MiB", heap, static_cast(budget.heapUsage[heap] >> 20u), static_cast(budget.heapBudget[heap] >> 20u)); + if (appended < 0 || static_cast(appended) >= remaining) break; + used += static_cast(appended); + } + } Check(result, details); return VK_NULL_HANDLE; } +VkDeviceSize BufferPool::ImageBudget(VkDeviceSize deviceBudget) { + static const char* value = std::getenv("APS5_IMAGE_POOL_MIB"); + if (value != nullptr) return static_cast(std::strtoull(value, nullptr, 10)) << 20u; + return deviceBudget / 4u; +} + +bool BufferPool::imagePooled(VkDeviceSize bytes, std::uint32_t memoryType) const { + return imageBudget != 0 && bytes != 0 && bytes <= imageBudget && bytes <= deviceTier.budget && memoryType < memoryProperties.memoryTypeCount && DeviceTiered(memoryProperties.memoryTypes[memoryType].propertyFlags); +} + +bool BufferPool::heapOverBudget() { + if (memoryProperties2 == nullptr || physical == VK_NULL_HANDLE) return false; + const auto now = std::chrono::steady_clock::now(); + if (heapRead != std::chrono::steady_clock::time_point{} && now - heapRead < std::chrono::milliseconds(100)) return heapOver; + VkPhysicalDeviceMemoryBudgetPropertiesEXT reported{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT}; + VkPhysicalDeviceMemoryProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2}; + properties.pNext = &reported; + memoryProperties2(physical, &properties); + heapRead = now; + heapOver = false; + for (std::uint32_t heap = 0; heap < memoryProperties.memoryHeapCount && heap < VK_MAX_MEMORY_HEAPS; ++heap) { + if ((memoryProperties.memoryHeaps[heap].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) != 0 && reported.heapBudget[heap] != 0 && reported.heapUsage[heap] > reported.heapBudget[heap]) heapOver = true; + } + return heapOver; +} + +VkDeviceMemory BufferPool::TakeImageMemory(const VkMemoryAllocateInfo& allocation, const char* operation, std::optional hostType, std::uint32_t* allocatedType) { + const auto type = allocation.memoryTypeIndex; + if (imagePooled(allocation.allocationSize, type)) { + std::lock_guard lock(mutex); + ++clock; + const auto found = deviceTier.free.find({static_cast(allocation.allocationSize), 0, memoryProperties.memoryTypes[type].propertyFlags, type}); + if (found != deviceTier.free.end() && !found->second.empty()) { + auto& slots = found->second; + const auto memory = slots.back().allocation.memory; + imageOrder.erase(slots.back().order); + slots.pop_back(); + if (slots.empty()) deviceTier.free.erase(found); + --deviceTier.slots; + deviceTier.retainedBytes -= allocation.allocationSize; + --imageSlots; + imageBytes -= allocation.allocationSize; + ++imageHits; + if (allocatedType != nullptr) *allocatedType = type; + return memory; + } + ++imageMisses; + } + return AllocateMemory(allocation, operation, hostType, allocatedType); +} + +void BufferPool::PutImageMemory(VkDeviceMemory memory, VkDeviceSize bytes, std::uint32_t memoryType) noexcept { + if (memory == VK_NULL_HANDLE) return; + if (!imagePooled(bytes, memoryType)) { + freeMemory(device, memory, nullptr); + return; + } + BufferAllocation allocation{VK_NULL_HANDLE, memory, nullptr, 0, bytes, static_cast(bytes), 0, memoryProperties.memoryTypes[memoryType].propertyFlags}; + allocation.memoryType = memoryType; + std::vector evicted; + try { + std::lock_guard lock(mutex); + if (heapOverBudget()) { + while (imageSlots != 0 && evictOldestImage(evicted)) { + } + evicted.push_back(allocation); + } else { + while (imageSlots != 0 && imageBytes + bytes > imageBudget && evictOldestImage(evicted)) { + } + while (deviceTier.slots != 0 && (deviceTier.retainedBytes + bytes > deviceTier.budget || deviceTier.slots >= maxSlots)) evictOldest(deviceTier, evicted); + const SlotKey imageKey{allocation.bytes, 0, allocation.properties, memoryType}; + auto& slots = deviceTier.free[imageKey]; + slots.push_back({allocation, ++clock, {}}); + try { + slots.back().order = imageOrder.insert(imageOrder.end(), &slots.back()); + } catch (...) { + slots.pop_back(); + if (slots.empty()) deviceTier.free.erase(imageKey); + throw; + } + ++deviceTier.slots; + deviceTier.retainedBytes += bytes; + ++imageSlots; + imageBytes += bytes; + } + } catch (...) { + destroy(allocation); + } + for (const auto& gone : evicted) destroy(gone); +} + +std::size_t BufferPool::RetainedImages() { + std::lock_guard lock(mutex); + return imageSlots; +} + VkDeviceSize BufferPool::ReclaimTarget(VkDeviceSize bytes) { return bytes + std::max(bytes, VkDeviceSize{256} << 20u); } @@ -161,11 +275,12 @@ SlabSlot BufferPool::TakeSlot(const Context& context, std::uint32_t memoryType, for (std::uint32_t index = slots; index-- != 0;) block->free.push_back(index); std::lock_guard lock(slabMutex); auto& slab = slabs[key]; - if (slab.available.size() == slab.available.capacity()) slab.available.reserve(std::max(1, slab.available.capacity() * 2)); + if (slab.blocks == slab.available.capacity()) slab.available.reserve(std::max(1, slab.available.capacity() * 2)); auto& inserted = *block; const auto owner = slabBlocks.try_emplace(inserted.memory).first; owner->second.reset(block.release()); slab.available.push_back(&inserted); + ++slab.blocks; ++slab.emptyBlocks; return takeFrom(*slab.available.back(), slab); } @@ -191,6 +306,7 @@ VkDeviceSize BufferPool::PutSlot(VkDeviceMemory memory, VkDeviceSize offset) noe return 0; } slab.available.erase(std::find(slab.available.begin(), slab.available.end(), &block)); + --slab.blocks; released = std::move(found->second); slabBlocks.erase(found); } @@ -241,7 +357,7 @@ VkDeviceSize BufferPool::destroy(const BufferAllocation& allocation) noexcept { } // Device-local allocations (see DeviceBuffer) are never mapped. if (allocation.mapping != nullptr) unmap(device, allocation.memory); - destroyBuffer(device, allocation.buffer, nullptr); + if (allocation.buffer != VK_NULL_HANDLE) destroyBuffer(device, allocation.buffer, nullptr); freeMemory(device, allocation.memory, nullptr); return allocation.allocationBytes; } @@ -282,7 +398,7 @@ std::optional BufferPool::Take(std::size_t bytes, VkBufferUsag const auto now = std::chrono::steady_clock::now(); if (now - lastReport > std::chrono::seconds(10)) { lastReport = now; - AgcDriver::ProfilePrint_nid_no_patch("[bufferpool] small: %llu hits, %llu misses, %llu evictions, %zu retained (%.0f MiB); large: %llu hits, %llu misses, %llu evictions, %zu retained (%.0f MiB)%s; device: %llu hits, %llu misses, %llu evictions, %zu retained (%.0f MiB of %llu)\n", static_cast(smallTier.hits), static_cast(smallTier.misses), static_cast(smallTier.evictions), smallTier.slots, smallTier.retainedBytes / 1048576.0, static_cast(largeTier.hits), static_cast(largeTier.misses), static_cast(largeTier.evictions), largeTier.slots, largeTier.retainedBytes / 1048576.0, sharedTiers() ? " (shared)" : "", static_cast(deviceTier.hits), static_cast(deviceTier.misses), static_cast(deviceTier.evictions), deviceTier.slots, deviceTier.retainedBytes / 1048576.0, static_cast(deviceTier.budget >> 20u)); + AgcDriver::ProfilePrint_nid_no_patch("[bufferpool] small: %llu hits, %llu misses, %llu evictions, %zu retained (%.0f MiB); large: %llu hits, %llu misses, %llu evictions, %zu retained (%.0f MiB)%s; device: %llu hits, %llu misses, %llu evictions, %zu retained (%.0f MiB of %llu); images: %llu hits, %llu misses, %llu evictions, %zu retained (%.0f MiB of %llu)\n", static_cast(smallTier.hits), static_cast(smallTier.misses), static_cast(smallTier.evictions), smallTier.slots, smallTier.retainedBytes / 1048576.0, static_cast(largeTier.hits), static_cast(largeTier.misses), static_cast(largeTier.evictions), largeTier.slots, largeTier.retainedBytes / 1048576.0, sharedTiers() ? " (shared)" : "", static_cast(deviceTier.hits), static_cast(deviceTier.misses), static_cast(deviceTier.evictions), deviceTier.slots, deviceTier.retainedBytes / 1048576.0, static_cast(deviceTier.budget >> 20u), static_cast(imageHits), static_cast(imageMisses), static_cast(imageEvictions), imageSlots, imageBytes / 1048576.0, static_cast(imageBudget >> 20u)); } } auto& tier = tierFor(capacity, properties); @@ -307,12 +423,37 @@ void BufferPool::evictOldest(Tier& tier, std::vector& evicted) // First: a throw here leaves the slot retained and counted. evicted.push_back(oldest->second.front().allocation); tier.retainedBytes -= oldest->second.front().allocation.allocationBytes; + if (oldest->second.front().allocation.buffer == VK_NULL_HANDLE) { + imageOrder.erase(oldest->second.front().order); + imageBytes -= oldest->second.front().allocation.allocationBytes; + --imageSlots; + ++imageEvictions; + } oldest->second.pop_front(); if (oldest->second.empty()) tier.free.erase(oldest); --tier.slots; ++tier.evictions; } +bool BufferPool::evictOldestImage(std::vector& evicted) { + if (imageOrder.empty()) return false; + const Slot* oldest = imageOrder.front(); + const SlotKey key{oldest->allocation.bytes, 0, oldest->allocation.properties, oldest->allocation.memoryType}; + const auto found = deviceTier.free.find(key); + evicted.push_back(oldest->allocation); + imageOrder.pop_front(); + const auto bytes = oldest->allocation.allocationBytes; + deviceTier.retainedBytes -= bytes; + imageBytes -= bytes; + found->second.pop_front(); + if (found->second.empty()) deviceTier.free.erase(found); + --deviceTier.slots; + --imageSlots; + ++deviceTier.evictions; + ++imageEvictions; + return true; +} + std::size_t BufferPool::MaxSlots() { // APS5_BUFFER_POOL_SLOTS= bounds the retained allocations; 64 is the capacity the pool had // before least-recently-used retention, for A/B runs. @@ -336,6 +477,10 @@ VkDeviceSize BufferPool::trim() { tier->retainedBytes = 0; tier->slots = 0; } + imageEvictions += imageSlots; + imageBytes = 0; + imageSlots = 0; + imageOrder.clear(); } VkDeviceSize bytes = 0; for (const auto* tier : {&retiredSmall, &retiredLarge, &retiredDevice}) { @@ -351,6 +496,7 @@ VkDeviceSize BufferPool::trim() { if (found == slabBlocks.end()) break; auto& slab = slabs.at(found->second->slab); slab.available.erase(std::find(slab.available.begin(), slab.available.end(), found->second.get())); + --slab.blocks; --slab.emptyBlocks; released = std::move(found->second); slabBlocks.erase(found); @@ -372,7 +518,7 @@ void BufferPool::Put(const BufferAllocation& allocation) noexcept { evicted.push_back(allocation); } else { while (tier.slots != 0 && (tier.retainedBytes + allocation.allocationBytes > tier.budget || tier.slots >= maxSlots)) evictOldest(tier, evicted); - tier.free[{allocation.bytes, allocation.usage, allocation.properties}].push_back({allocation, ++clock}); + tier.free[{allocation.bytes, allocation.usage, allocation.properties}].push_back({allocation, ++clock, {}}); ++tier.slots; tier.retainedBytes += allocation.allocationBytes; } @@ -387,8 +533,17 @@ std::shared_ptr GetBufferPool(const Context& context) { return context.bufferPool; } -VkDeviceMemory AllocateImageMemory(const Context& context, const VkMemoryAllocateInfo& allocation, std::uint32_t mask, const char* operation) { - return GetBufferPool(context)->AllocateMemory(allocation, operation, context.RefusedDeviceMemoryFallback(mask, allocation.memoryTypeIndex)); +VkDeviceMemory AllocateImageMemory(const Context& context, const VkMemoryAllocateInfo& allocation, std::uint32_t mask, const char* operation, std::uint32_t* allocatedType) { + return GetBufferPool(context)->TakeImageMemory(allocation, operation, context.RefusedDeviceMemoryFallback(mask, allocation.memoryTypeIndex), allocatedType); +} + +void ReleaseImageMemory(const Context& context, VkDeviceMemory memory, VkDeviceSize bytes, std::uint32_t memoryType) noexcept { + if (memory == VK_NULL_HANDLE) return; + if (context.bufferPool) { + context.bufferPool->PutImageMemory(memory, bytes, memoryType); + return; + } + context.Function("vkFreeMemory")(context.device, memory, nullptr); } } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp index 043bb2697a..eee078cba1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp @@ -611,6 +611,22 @@ bool IsDccClear(DccKeys keys) { return keys == DccKeys::Clear0000 || keys == DccKeys::Clear0001 || keys == DccKeys::Clear1110 || keys == DccKeys::Clear1111 || keys == DccKeys::ClearRegister; } +std::optional LandedUniformMetadataByte(std::uint64_t address, std::size_t bytes) { + if (address == 0 || bytes == 0 || !GuestMemory::Accessible(reinterpret_cast(address), bytes)) return std::nullopt; + if (AnyShadowedOverlaps(address, bytes)) PublishShadow(address, bytes, PublishScope::Whole, PublishReason::Keys); + if (AnyShadowedOverlaps(address, bytes)) return std::nullopt; + GuestMemory::FlushGpuWrites(address, bytes); + auto* recorder = GuestMemory::GpuMutex().HeldByThisThread() ? Recorder::Active() : nullptr; + if (recorder != nullptr && recorder->PendingWriteOverlaps(address, bytes)) { + const auto landed = KeysAfterPendingWrites(*recorder, address, bytes); + if (landed.mixed || landed.unknown || landed.value < 0) return std::nullopt; + return static_cast(landed.value); + } + const auto* data = reinterpret_cast(address); + if (!AllKeysEqual(data + 1, bytes - 1, data[0])) return std::nullopt; + return data[0]; +} + void MarkDccUncompressed(std::uint64_t metaAddress, std::uint64_t surfaceBytes) { MarkDccUncompressed(metaAddress, surfaceBytes, DccKeyBytes(surfaceBytes)); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index c7899bb128..3c31c3ab57 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -33,6 +33,7 @@ namespace AgcDriver::Graphics { namespace { std::uint64_t depthWrites = 0; +std::uint64_t mirrorEpoch = 0; std::pair DepthBlockExtent(std::uint32_t bytesPerTexel) { return {bytesPerTexel == 4 ? 128u : 256u, bytesPerTexel == 1 ? 256u : 128u}; @@ -102,7 +103,7 @@ DepthSamplingState prepareDepthSampling(const Context& context, const DepthTarge if ((target.pipeBankXor & ~0xcf00u) != 0u) return refuse("unsupported native multisample depth XOR"); if (target.address == 0u || (target.address & 65535u) != 0u) return refuse("native multisample depth base is not aligned"); if (target.stencilAddress != 0u || target.htileStencil) return refuse("native multisample stencil import is not implemented"); - if (target.htileAddress == 0u || (target.htileAddress & 255u) != 0u || !target.iterate256 || !target.htilePipeAligned || target.htileVrsEncoding != 0u) return refuse("native multisample depth initial import requires pipe-aligned ITERATE_256 HTILE without VRS"); + if (target.htileAddress == 0u || (target.htileAddress & 255u) != 0u || !target.htilePipeAligned || target.htileVrsEncoding != 0u) return refuse("native multisample depth initial import requires pipe-aligned HTILE without VRS"); if (target.clearDepth != 0.0f && target.clearDepth != 1.0f) return refuse("native multisample depth initial clear value is not implemented"); state.initial = {{{target.address, static_cast(MultisampledDepthBytes_nid_no_patch(target.extent, target.format == VK_FORMAT_D16_UNORM ? 2u : 4u, target.samples)), 0u}, {target.htileAddress, static_cast(HtileMetadataBytes(target.extent)), 0u}}}; for (const auto& range : state.initial) { @@ -252,12 +253,22 @@ public: sampleable = target.samples == 1; if (target.samples != 1) { Require(context.imageFormatProperties != nullptr, "native multisample depth requires image format queries"); - VkImageFormatProperties supported{}; - const auto result = context.imageFormatProperties(context.physical, hostFormat, info.imageType, info.tiling, info.usage | VK_IMAGE_USAGE_SAMPLED_BIT, info.flags, &supported); - if (result != VK_ERROR_FORMAT_NOT_SUPPORTED) Check(result, "vkGetPhysicalDeviceImageFormatProperties sampled depth"); - sampleable = (properties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) != 0 && result == VK_SUCCESS && (supported.sampleCounts & info.samples) != 0 && supported.maxMipLevels >= 1u && supported.maxArrayLayers >= 1u && supported.maxExtent.depth >= 1u && info.extent.width <= supported.maxExtent.width && info.extent.height <= supported.maxExtent.height; + VkImageFormatProperties shadowProperties{}; + VkImageFormatProperties directProperties{}; + const auto sampledUsage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + const auto shadowResult = context.imageFormatProperties(context.physical, hostFormat, info.imageType, info.tiling, sampledUsage, 0, &shadowProperties); + if (shadowResult != VK_ERROR_FORMAT_NOT_SUPPORTED) Check(shadowResult, "vkGetPhysicalDeviceImageFormatProperties sampled depth shadow"); + const auto directResult = context.imageFormatProperties(context.physical, hostFormat, info.imageType, info.tiling, info.usage | VK_IMAGE_USAGE_SAMPLED_BIT, info.flags, &directProperties); + if (directResult != VK_ERROR_FORMAT_NOT_SUPPORTED) Check(directResult, "vkGetPhysicalDeviceImageFormatProperties sampled depth attachment"); + const auto supports = [&](VkResult result, const VkImageFormatProperties& supported) { + return (properties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) != 0 && result == VK_SUCCESS && (supported.sampleCounts & info.samples) != 0 && supported.maxMipLevels >= 1u && supported.maxArrayLayers >= 1u && supported.maxExtent.depth >= 1u && info.extent.width <= supported.maxExtent.width && info.extent.height <= supported.maxExtent.height; + }; + useSampleShadow = supports(shadowResult, shadowProperties); + const bool directSampleable = supports(directResult, directProperties); + sampleable = useSampleShadow || directSampleable; + if (!useSampleShadow && directSampleable) info.usage |= VK_IMAGE_USAGE_SAMPLED_BIT; } - if (sampleable) info.usage |= VK_IMAGE_USAGE_SAMPLED_BIT; + if (target.samples == 1 && sampleable) info.usage |= VK_IMAGE_USAGE_SAMPLED_BIT; info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; Check(context.Function("vkCreateImage")(context.device, &info, nullptr, &image), "vkCreateImage depth"); @@ -407,7 +418,6 @@ public: key[10] = components.b; key[11] = components.a; key[12] = static_cast(multisampleViewType); - if (const auto found = textures.find(key); found != textures.end()) return found->second; const bool multisampled = target.samples != 1; Require(!multisampled || sampleable, "the device cannot sample the native depth target's format and sample count"); const bool stencil = target.stencilAddress != 0 && resource.baseAddress == target.stencilAddress; @@ -430,8 +440,14 @@ public: static_cast(resource.tileMode), static_cast(resource.dimension), resource.baseLevel, resource.lastLevel, resource.baseArray, key[0], key[1], key[2], key[3], key[4], key[5], key[6], key[7]); throw std::runtime_error(text); } + if (multisampled && useSampleShadow) { + if (sampleShadow == nullptr) createSampleShadow(); + refreshSampleShadow(drawn); + } + if (const auto found = textures.find(key); found != textures.end()) return found->second; const auto viewType = multisampled ? multisampleViewType : resource.dimension == TextureDimension::k2DArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D; - auto texture = std::make_shared(context, image, hostFormat, stencil ? VK_IMAGE_ASPECT_STENCIL_BIT : VK_IMAGE_ASPECT_DEPTH_BIT, components, viewType, 0, owned); + const auto sampledImage = useSampleShadow ? sampleShadow : owned; + auto texture = std::make_shared(context, useSampleShadow ? sampleShadow->image : image, hostFormat, stencil ? VK_IMAGE_ASPECT_STENCIL_BIT : VK_IMAGE_ASPECT_DEPTH_BIT, components, viewType, 0, sampledImage); if (multisampled) { Require(samplingActive, "native multisample depth sampling requires its creation-time owner"); texture->MarkRefreshedPerUse(); @@ -682,6 +698,43 @@ public: else Recorder::CountBarriers(Recorder::CommandClass::Copy, 3); } + std::shared_ptr StoreDepthPlane(const GuestTextureResource& resource) { + ApplyFastClear(); + if (mirrorPlane.storage == nullptr) { + mirrorPlane.storage = StorageTexture::Create(context, *context.detiler, resource, 0); + ++mirrorEpoch; + } + copyPlane(mirrorPlane, mirrorPlane.storage, false, true, true); + return mirrorPlane.storage; + } + + bool SyncFromMemory(const GuestTextureResource& resource) { + const auto bytes = static_cast(DescribeSurface(resource).guestBytes); + GuestMemory::CollectWrites(resource.baseAddress, bytes); + if (syncedGeneration == 0 || (pendingClear & VK_IMAGE_ASPECT_DEPTH_BIT) != 0 || !GuestMemory::WrittenSince(resource.baseAddress, bytes, syncedGeneration)) { + syncedGeneration = GuestMemory::TrackerGeneration(); + return false; + } + memoryStale = false; + if (mirrorPlane.storage == nullptr) { + mirrorPlane.storage = StorageTexture::Create(context, *context.detiler, resource, 0); + ++mirrorEpoch; + } + else static_cast(mirrorPlane.storage->Refresh()); + syncedGeneration = GuestMemory::TrackerGeneration(); + copyPlane(mirrorPlane, mirrorPlane.storage, false, false, true); + written = ++depthWrites; + return true; + } + + void DropMirror() { + mirrorPlane = {}; + } + + bool memoryStale = false; + std::uint64_t scannedEpoch = 0; + std::uint64_t syncedGeneration = 0; + const Context context; const DepthTarget target; const VkFormat hostFormat; @@ -694,6 +747,9 @@ public: DepthSamplingState sampling; std::uint64_t clearGeneration = 0u; std::shared_ptr owned; + std::shared_ptr sampleShadow; + bool useSampleShadow = false; + std::uint64_t sampleShadowWritten = 0u; private: struct PlaneStorage { @@ -716,6 +772,76 @@ private: std::uint64_t stencilMemory = 0; StencilCopy stencilCopy; + void createSampleShadow() { + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + try { + VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + info.imageType = VK_IMAGE_TYPE_2D; + info.format = hostFormat; + info.extent = {target.extent.width, target.extent.height, 1}; + info.mipLevels = 1; + info.arrayLayers = 1; + info.samples = static_cast(target.samples); + info.tiling = VK_IMAGE_TILING_OPTIMAL; + info.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + Check(context.Function("vkCreateImage")(context.device, &info, nullptr, &image), "vkCreateImage sampled depth shadow"); + VkMemoryRequirements requirements{}; + context.Function("vkGetImageMemoryRequirements")(context.device, image, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; + allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory sampled depth shadow"); + Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory sampled depth shadow"); + sampleShadow = std::make_shared(context, image, memory); + image = VK_NULL_HANDLE; + memory = VK_NULL_HANDLE; + auto* recorder = RecorderFor(context); + if (recorder != nullptr) recorder->Keep(sampleShadow); + std::unique_ptr batch; + if (recorder == nullptr) batch = std::make_unique(context); + const auto commands = recorder != nullptr ? recorder->Commands() : batch->Handle(); + VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + barrier.newLayout = VK_IMAGE_LAYOUT_GENERAL; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = sampleShadow->image; + barrier.subresourceRange = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 0, 1}; + context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); + if (batch) batch->SubmitAndWait(); + else Recorder::CountBarriers(Recorder::CommandClass::Draw, 1); + } catch (...) { + if (image != VK_NULL_HANDLE) context.Function("vkDestroyImage")(context.device, image, nullptr); + if (memory != VK_NULL_HANDLE) context.Function("vkFreeMemory")(context.device, memory, nullptr); + throw; + } + } + + void refreshSampleShadow(bool drawn) { + Require(sampleShadow != nullptr, "native multisample depth shadow was not created"); + if (!drawn && sampleShadowWritten == written) return; + auto* recorder = RecorderFor(context); + std::unique_ptr batch; + if (recorder == nullptr) batch = std::make_unique(context); + const auto commands = recorder != nullptr ? recorder->Commands() : batch->Handle(); + if (recorder != nullptr) recorder->Keep(sampleShadow); + constexpr VkAccessFlags accesses = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, accesses, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + VkImageCopy region{}; + region.srcSubresource = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 0, 1}; + region.dstSubresource = region.srcSubresource; + region.extent = {target.extent.width, target.extent.height, 1}; + context.Function("vkCmdCopyImage")(commands, image, VK_IMAGE_LAYOUT_GENERAL, sampleShadow->image, VK_IMAGE_LAYOUT_GENERAL, 1, ®ion); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, accesses); + if (batch) batch->SubmitAndWait(); + else Recorder::CountBarriers(Recorder::CommandClass::Draw, 2); + sampleShadowWritten = written; + } + std::shared_ptr unormStencil(const std::array& key, VkComponentMapping components, VkImageViewType viewType, bool drawn) { if (stencilCopy.image == VK_NULL_HANDLE) { VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; @@ -805,6 +931,7 @@ private: std::uint64_t stencilBytes() const { return DepthSliceBytes(target.extent, 1u); } + PlaneStorage mirrorPlane; bool CoversPlane(const StorageTexture& storage, bool stencil) const { const auto& resource = storage.Descriptor(); @@ -816,15 +943,17 @@ private: return ResolveTextureFormat(resource.format) == (stencil ? VK_FORMAT_R8_UINT : d16 ? VK_FORMAT_R16_UNORM : VK_FORMAT_R32_SFLOAT); } - void copyPlane(PlaneStorage& plane, const std::shared_ptr& storage, bool stencil, bool toStorage) { + void copyPlane(PlaneStorage& plane, const std::shared_ptr& storage, bool stencil, bool toStorage, bool ownBatch = false) { auto* recorder = Recorder::Active(); - Require(recorder != nullptr, "depth surface storage copies need an active recorder"); + Require(recorder != nullptr || ownBatch, "depth surface storage copies need an active recorder"); + std::unique_ptr batch; + if (recorder == nullptr) batch = std::make_unique(context); const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; const auto bytes = static_cast(target.extent.width) * target.extent.height * (stencil ? 1u : d16 ? 2u : 4u); if (plane.staging == nullptr) plane.staging = std::make_unique(context, bytes, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); - const auto commands = recorder->Commands(); - recorder->Keep(storage); - const auto timing = recorder->BeginGpuTiming(Recorder::CommandClass::Copy); + const auto commands = recorder != nullptr ? recorder->Commands() : batch->Handle(); + if (recorder != nullptr) recorder->Keep(storage); + const std::uint32_t timing = recorder != nullptr ? recorder->BeginGpuTiming(Recorder::CommandClass::Copy) : 0u; VkBufferImageCopy planeRegion{0, 0, 0, {stencil ? VK_IMAGE_ASPECT_STENCIL_BIT : VK_IMAGE_ASPECT_DEPTH_BIT, 0, 0, 1}, {0, 0, 0}, {target.extent.width, target.extent.height, 1}}; VkBufferImageCopy colorRegion = planeRegion; colorRegion.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; @@ -835,6 +964,10 @@ private: RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); context.Function("vkCmdCopyBufferToImage")(commands, plane.staging->Handle(), destinationImage, VK_IMAGE_LAYOUT_GENERAL, 1, toStorage ? &colorRegion : &planeRegion); RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT); + if (batch) { + batch->SubmitAndWait(); + return; + } Recorder::CountBarriers(Recorder::CommandClass::Copy, 3); recorder->EndGpuTiming(timing, bytes); } @@ -843,6 +976,7 @@ private: depthPlane = {}; stencilPlane = {}; releaseStencilCopy(); + mirrorPlane = {}; textures.clear(); if (view) context.Function("vkDestroyImageView")(context.device, view, nullptr); if (owned != nullptr) owned.reset(); @@ -1135,8 +1269,8 @@ bool sameSurface(const DepthTarget& a, const DepthTarget& b) { return a.address == b.address && a.stencilAddress == b.stencilAddress && a.extent.width == b.extent.width && a.extent.height == b.extent.height && a.format == b.format && a.samples == b.samples && (a.samples == 1u || (a.htileAddress == b.htileAddress && a.htileStencil == b.htileStencil && a.depthSwizzle == b.depthSwizzle && a.pipeBankXor == b.pipeBankXor && a.firstSlice == b.firstSlice && a.lastSlice == b.lastSlice && a.iterate256 == b.iterate256 && a.htilePipeAligned == b.htilePipeAligned && a.htileVrsEncoding == b.htileVrsEncoding && a.htileDepthOnly == b.htileDepthOnly)); } -std::mutex& surfacesMutex() { - static std::mutex mutex; +std::recursive_mutex& surfacesMutex() { + static std::recursive_mutex mutex; return mutex; } @@ -1208,13 +1342,6 @@ DepthSurface* planeSurface(VkDevice device, std::uint64_t address) { return found == list.rend() ? nullptr : found->get(); } -void retire(DepthSurface& surface) { - surface.retired = true; - for (auto* owners : {&depthOwners(), &stencilOwners()}) { - std::erase_if(*owners, [&](const auto& entry) { return entry.second.surface == &surface; }); - } -} - std::map& clearedHtiles() { static auto* cleared = new std::map(); return *cleared; @@ -1229,7 +1356,7 @@ bool takeClearedHtile(const DepthTarget& target) { return true; } -const DepthSurface* drawnDepth = nullptr; +DepthSurface* drawnDepth = nullptr; std::atomic surfacesSerial{0}; std::atomic surfacesReleased{0}; @@ -1382,6 +1509,84 @@ VkImageAspectFlags storeBackClearedHtile(const Context& context, const DepthTarg htileReads()[target.htileAddress] = generation; return cleared; } + +struct StalePlanes { + std::mutex mutex; + std::vector> ranges; + std::atomic begin{std::numeric_limits::max()}; + std::atomic end{0}; +}; + +StalePlanes& stalePlanes() { + static auto* planes = new StalePlanes(); + return *planes; +} + +void publishStaleHull(StalePlanes& planes) { + auto begin = std::numeric_limits::max(); + std::uint64_t end = 0; + for (const auto& range : planes.ranges) { + begin = std::min(begin, range.first); + end = std::max(end, range.second); + } + planes.begin.store(begin, std::memory_order_release); + planes.end.store(end, std::memory_order_release); +} + +void noteStalePlane(std::uint64_t begin, std::uint64_t end) { + auto& planes = stalePlanes(); + std::lock_guard lock(planes.mutex); + if (std::find(planes.ranges.begin(), planes.ranges.end(), std::pair{begin, end}) != planes.ranges.end()) return; + planes.ranges.emplace_back(begin, end); + publishStaleHull(planes); +} + +bool overlapsStalePlane(std::uint64_t begin, std::uint64_t end) { + auto& planes = stalePlanes(); + if (begin >= planes.end.load(std::memory_order_acquire) || planes.begin.load(std::memory_order_acquire) >= end) return false; + std::lock_guard lock(planes.mutex); + return std::any_of(planes.ranges.begin(), planes.ranges.end(), [&](const auto& range) { return begin < range.second && range.first < end; }); +} + +bool mirrorable(const DepthTarget& target) { + return (target.format == VK_FORMAT_D32_SFLOAT || target.format == VK_FORMAT_D32_SFLOAT_S8_UINT) && target.samples == 1 && target.depthSwizzle == XorSwizzleMode(TextureTileMode::kZ64KBX); +} + +GuestTextureResource mirrorResource(const DepthTarget& target) { + GuestTextureResource resource{}; + resource.baseAddress = target.address; + resource.width = target.extent.width; + resource.height = target.extent.height; + resource.mipCount = 1; + resource.allocatedMipCount = 1; + resource.tileMode = TextureTileMode::kZ64KBX; + resource.dimension = TextureDimension::k2D; + resource.format = 22; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + return resource; +} + +std::pair stalePlaneRange(const DepthSurface& surface) { + return {surface.target.address, surface.target.address + DescribeSurface(mirrorResource(surface.target)).guestBytes}; +} + +void retire(DepthSurface& surface) { + surface.retired = true; + for (auto* owners : {&depthOwners(), &stencilOwners()}) { + std::erase_if(*owners, [&](const auto& entry) { return entry.second.surface == &surface; }); + } + if (!surface.memoryStale) return; + surface.memoryStale = false; + const auto plane = stalePlaneRange(surface); + if (std::any_of(surfaces().begin(), surfaces().end(), [&](const auto& other) { return other->memoryStale && stalePlaneRange(*other) == plane; })) return; + auto& planes = stalePlanes(); + std::lock_guard lock(planes.mutex); + std::erase(planes.ranges, plane); + publishStaleHull(planes); +} } std::uint64_t DepthSurfacesSerial() { @@ -1486,7 +1691,7 @@ float HostDepthBiasConstant(const Context& context, VkFormat format, float const return constant * 256.0f; } -VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target) { +VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target, bool depthWritten) { const auto htileCleared = RoutesToNativeDepthOwner(target) ? VkImageAspectFlags{0} : storeBackClearedHtile(context, target); std::lock_guard gpu(GuestMemory::GpuMutex()); std::unique_lock lock(surfacesMutex()); @@ -1521,6 +1726,7 @@ VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target) releaseRetired(context); surfacesSerial.fetch_add(1, std::memory_order_release); surfaces().push_back(std::make_unique(context, target, std::move(sampling))); + ++mirrorEpoch; auto& fresh = *surfaces().back(); if (target.samples != 1u && fresh.sampleable && fresh.sampling.refusal.empty()) { std::unique_lock guardLock(depthGuardMutex()); @@ -1543,13 +1749,108 @@ VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target) if (!created) surface->NoteHtileClear(VK_IMAGE_ASPECT_DEPTH_BIT); } if (!created) surface->NoteHtileClear(htileCleared); + if (mirrorable(target)) static_cast(surface->SyncFromMemory(mirrorResource(target))); surface->ApplyFastClear(); - if (!created || inherits) surface->written = ++depthWrites; + if (drawnDepth != nullptr && drawnDepth != surface) drawnDepth->written = ++depthWrites; + surface->written = ++depthWrites; surface->NoteMemory(); + if (mirrorable(target) && surface->scannedEpoch != mirrorEpoch) { + surface->scannedEpoch = mirrorEpoch; + const auto plane = stalePlaneRange(*surface); + for (const auto& other : surfaces()) { + if (other.get() == surface || other->context.device != context.device || other->written >= surface->written || !mirrorable(other->target)) continue; + const auto otherPlane = stalePlaneRange(*other); + if (otherPlane == plane || otherPlane.first >= plane.second || plane.first >= otherPlane.second) continue; + other->memoryStale = false; + other->DropMirror(); + } + } + if ((depthWritten || cleared) && mirrorable(target)) { + surface->memoryStale = true; + const auto plane = target.address; + noteStalePlane(plane, plane + DescribeSurface(mirrorResource(target)).guestBytes); + } drawnDepth = surface; return surface->view; } +SampleOrderTarget DepthSurfaceOrderTarget(const Context& context, const DepthTarget& target) { + std::lock_guard gpu(GuestMemory::GpuMutex()); + std::unique_lock lock(surfacesMutex()); + for (const auto& candidate : surfaces()) { + if (!candidate->active->load(std::memory_order_acquire) || candidate->context.device != context.device || !sameSurface(candidate->target, target)) continue; + Require(candidate->sampleable, "the multisampled depth surface whose samples are put in the guest's order cannot be sampled"); + return {candidate->image, candidate->view, candidate->hostFormat, target.extent, target.samples, DepthSampleOrderMethod(context, target), candidate->owned}; + } + throw std::runtime_error("AGC graphics: the multisampled depth surface whose samples are put in the guest's order is not resident"); +} + +DepthSampleLocations DepthSurfaceSampleLocations(const Context& context, const DepthTarget& target) { + std::lock_guard gpu(GuestMemory::GpuMutex()); + std::unique_lock lock(surfacesMutex()); + for (const auto& candidate : surfaces()) { + if (!candidate->active->load(std::memory_order_acquire) || candidate->context.device != context.device || !sameSurface(candidate->target, target)) continue; + DepthSampleLocations depth; + depth.image = candidate->image; + depth.format = candidate->hostFormat; + depth.samples = target.samples; + depth.keep = candidate->owned; + return depth; + } + throw std::runtime_error("AGC graphics: the depth surface drawn at custom sample locations is not resident"); +} + +bool DepthPlanesStale() { + auto& planes = stalePlanes(); + return planes.begin.load(std::memory_order_acquire) < planes.end.load(std::memory_order_acquire); +} + +std::vector> FlushDepthPlanes(std::uint64_t address, std::size_t bytes) { + static thread_local bool flushing = false; + const auto end = bytes > std::numeric_limits::max() - address ? std::numeric_limits::max() : address + bytes; + if (flushing || !overlapsStalePlane(address, end)) return {}; + flushing = true; + struct Reset { + ~Reset() { flushing = false; } + } reset; + std::vector>> stored; + std::vector> ranges; + { + std::lock_guard gpu(GuestMemory::GpuMutex()); + std::lock_guard lock(surfacesMutex()); + std::vector> remaining; + for (const auto& surface : surfaces()) { + if (!surface->memoryStale) continue; + const auto resource = mirrorResource(surface->target); + const auto plane = surface->target.address; + const auto planeBytes = DescribeSurface(resource).guestBytes; + if (plane >= end || address >= plane + planeBytes) { + remaining.emplace_back(plane, plane + planeBytes); + continue; + } + surface->memoryStale = false; + if (surface->SyncFromMemory(resource)) continue; + stored.emplace_back(surface.get(), surface->StoreDepthPlane(resource)); + ranges.emplace_back(plane, planeBytes); + } + auto& planes = stalePlanes(); + std::lock_guard staleLock(planes.mutex); + planes.ranges = std::move(remaining); + publishStaleHull(planes); + } + for (const auto& entry : stored) entry.second->MarkWholeWritten(); + for (const auto& [plane, planeBytes] : ranges) StorageTexture::FlushPending(plane, static_cast(planeBytes), nullptr, "depth plane store"); + const auto generation = GuestMemory::TrackerGeneration(); + std::lock_guard lock(surfacesMutex()); + for (const auto& entry : stored) { + if (std::any_of(surfaces().begin(), surfaces().end(), [&](const auto& candidate) { return candidate.get() == entry.first; })) { + entry.first->syncedGeneration = generation; + entry.first->NoteMemory(); + } + } + return ranges; +} + std::uint64_t HtileDepthClearAddress(std::span code, std::span userData, const std::array& numThreads) { static constexpr std::array htileMaskKernel{0xd7460000u, 0x04010c06u, 0x34000084u, 0xdc388000u, 0x04020000u, 0xbf8c3f70u, 0xd7710008u, 0x00120805u, 0xd7710009u, 0x00120a05u, 0xd771000au, 0x00120c05u, 0xd771000bu, 0x00120e05u, 0xdc788000u, 0x00020800u, 0xbf810000u}; if (userData.size() < 6 || numThreads[0] != 64 || numThreads[1] != 1 || numThreads[2] != 1) return 0; @@ -1712,7 +2013,7 @@ std::shared_ptr DepthSurfaceTexture(const Context& context, std::spanpendingClear == 0u, "native multisample depth fast-clear transition is not implemented"); RecordMemoryBarrier(context, recorder->Commands(), VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); Recorder::CountBarriers(Recorder::CommandClass::Draw, 1); - return found->Sampled(words, resource, components, false, multisampleViewType); + return found->Sampled(words, resource, components, found == drawnDepth, multisampleViewType); } if (resource.width != base.extent.width || resource.height != base.extent.height || resource.baseLevel != 0) return nullptr; const bool d16 = base.format == VK_FORMAT_D16_UNORM || base.format == VK_FORMAT_D16_UNORM_S8_UINT; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 1fb537a91b..7822683c32 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -8,6 +8,7 @@ #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/SampleOrder.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include "prx/libSceAgcDriver/Graphics/include/UnitShadow.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" @@ -304,7 +305,7 @@ bool materializeCmaskClear(const Context& context, const ColorTarget& color, con return filled; } -void imageBarrier(const Context& context, VkCommandBuffer commands, VkImage image, VkImageLayout oldLayout, VkImageLayout newLayout, VkPipelineStageFlags sourceStage, VkPipelineStageFlags destinationStage, VkAccessFlags sourceAccess, VkAccessFlags destinationAccess) { +void imageBarrier(const Context& context, VkCommandBuffer commands, VkImage image, VkImageLayout oldLayout, VkImageLayout newLayout, VkPipelineStageFlags sourceStage, VkPipelineStageFlags destinationStage, VkAccessFlags sourceAccess, VkAccessFlags destinationAccess, VkImageSubresourceRange subresources = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}) { VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; barrier.srcAccessMask = sourceAccess; barrier.dstAccessMask = destinationAccess; @@ -313,7 +314,7 @@ void imageBarrier(const Context& context, VkCommandBuffer commands, VkImage imag barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.image = image; - barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + barrier.subresourceRange = subresources; context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, sourceStage, destinationStage, 0, 0, nullptr, 0, nullptr, 1, &barrier); } @@ -654,6 +655,7 @@ bool ValidationKey(const Context& context, std::span shade add(state.rectList); add(state.topology); add(state.cullMode); + add(state.polygonMode); add(state.blends.size()); add(context.subgroup.subgroupSize); add(context.subgroup.supportedStages); @@ -803,10 +805,11 @@ void CheckBufferAliases(std::span shaders, const ColorTarg for (std::size_t offset = 0; offset + 4 <= words.size(); offset += 4) { const ShaderRecompiler::ShaderBufferResource descriptor{{words[offset], words[offset + 1], words[offset + 2], words[offset + 3]}}; const auto address = descriptor.Base48(); - const auto size = descriptor.GetSize(); + auto size = descriptor.GetSize(); if (size == 0 || address == 0) continue; const auto element = offset / 4; const bool written = element >= binding.bufferWritten.size() || binding.bufferWritten[element]; + if (const std::uint64_t stride = descriptor.Stride(); !written && stride != 0 && address < target.address && address + size > target.address && address + size - target.address < stride) size = target.address - address; Require(!overlap(address, size, target.address, target.bytes), "shader buffer aliases the render target"); Require(!written || !overlap(address, size, indexAddress, indexBytes), "writable shader buffer aliases the index buffer"); } @@ -1648,7 +1651,7 @@ struct Kept { // The completion side of a recorded draw: the kept objects, the record check, the lease outcome, // the GPU write notes, the copied-buffer write-back (listed in DrawCopiedWriters or run as a // completion action) and the targets marked dirty. -void keepRecordedDraw(Recorder& recorder, const std::shared_ptr& resources, std::shared_ptr pipeline, std::shared_ptr framebuffer, DrawInputs& inputs, std::vector> targets, std::vector slices, std::unique_ptr scratch, std::function checkRecords, bool listed, bool completion, const DrawOutcome& outcome) { +void keepRecordedDraw(Recorder& recorder, const std::shared_ptr& resources, std::shared_ptr pipeline, std::shared_ptr framebuffer, DrawInputs& inputs, std::vector> targets, std::vector> slices, std::unique_ptr scratch, std::function checkRecords, bool listed, bool completion, const DrawOutcome& outcome) { auto kept = std::make_shared(); kept->resources = resources; kept->pipeline = std::move(pipeline); @@ -1689,7 +1692,7 @@ void keepRecordedDraw(Recorder& recorder, const std::shared_ptr } for (std::size_t index = 0; index < residents.size(); ++index) { if (residents[index] == nullptr) continue; - if (index < slices.size()) residents[index]->MarkSliceDirty(slices[index]); + if (index < slices.size()) residents[index]->MarkSliceDirty(slices[index].first, slices[index].second); else residents[index]->MarkDirty(); } for (const auto& resident : residents) { @@ -1709,7 +1712,7 @@ struct RecordedDraw { std::shared_ptr framebuffer; std::span targetViews; std::vector> targets; - std::vector targetSlices; + std::vector> targetSlices; std::vector> multisampleTargets; std::vector> proxies; std::vector multisampleImages; @@ -1721,6 +1724,9 @@ struct RecordedDraw { const std::array* pushBytes = nullptr; VkShaderStageFlags pushStages = 0; std::vector>> stencilReplay; + std::vector reordered; + SampleOrderPass reorderPass; + std::optional depthLocations; }; // A remapped viewport depth range reads its scale and offset from the draw's push block (the vertex shader's @@ -1880,6 +1886,11 @@ void captureInputs(const Context& context, Recorder& recorder, VkCommandBuffer c }); } +bool WriteOnlyPasses() { + static const bool enabled = std::getenv("APS5_NO_WRITE_ONLY_PASSES") == nullptr; + return enabled; +} + void recordDraw(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span shaders, DrawInputs& inputs, RecordedDraw& record, DrawOutcome& outcome, DrawTimer& timer, double& ownWaitedMs) { auto* recorder = record.recorder; auto& resources = *record.resources; @@ -1919,6 +1930,7 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam auto& passReads = ShaderRecompiler::ThreadOwned(passReadsSlot); passReads.clear(); if (!addressBased) resources.DeviceReads(passReads); + const auto deviceReads = passReads.size(); if (gpuIndirect) { passReads.emplace_back(args->arguments, args->arguments + args->RangeBytes()); if (args->countIndirect) passReads.emplace_back(args->countAddress, args->countAddress + 4); @@ -1936,7 +1948,29 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam const bool readOnlyBda = addressReadPasses && resources.BdaReadOnly(); outcome.addressDraw = addressBased; const bool unknownWrites = ((addressBased || resources.UsesAddressTable()) && !readOnlyBda) || resources.HasCopiedWrites(); - const PassAccess passAccess{passReads, resources.GpuWrites(), passImages, passAttachments, unknownMemory, unknownWrites}; + GuestRangeList passWrites = resources.GpuWrites(); + GuestRangeList passWriteOnly{}; + thread_local std::vector>* hazardWritesSlot = nullptr; + if (const auto& writeOnly = resources.WriteOnlyRanges(); !writeOnly.empty() && !unknownMemory && WriteOnlyPasses()) { + const auto listed = [&](const std::pair& range, bool aligned) { + return std::any_of(writeOnly.begin(), writeOnly.end(), [&](const auto& only) { + if (!aligned) return only == range; + const auto begin = only.first & ~std::uint64_t{3}; + const auto end = begin == only.first ? only.second : (only.second + 3) & ~std::uint64_t{3}; + return range.first == begin && range.second == end; + }); + }; + const auto ownReads = passReads.begin() + static_cast(deviceReads); + passReads.erase(std::remove_if(passReads.begin(), ownReads, [&](const auto& range) { return listed(range, true); }), ownReads); + auto& hazardWrites = ShaderRecompiler::ThreadOwned(hazardWritesSlot); + hazardWrites.clear(); + for (const auto& range : resources.GpuWrites()) { + if (!listed(range, false)) hazardWrites.push_back(range); + } + passWrites = hazardWrites; + passWriteOnly = writeOnly; + } + const PassAccess passAccess{passReads, passWrites, passImages, passAttachments, unknownMemory, unknownWrites, passWriteOnly}; const bool fenced = state.depth.has_value() || !record.proxies.empty(); const auto start = recorder->StartDrawPass(passKey, capture || readsTarget || gpuIndirect || meshIndirect, fenced, passAccess); for (const auto& target : record.multisampleTargets) KeepMultisampleTarget_nid_no_patch(target); @@ -1993,6 +2027,11 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam } // Earlier recorded work (dispatches, the previous draw) wrote the images in the general // layout, which a lean draw renders in: no transitions. + if (!record.reordered.empty()) { + std::vector> kept; + RecordSampleOrder(context, commands, record.reorderPass, record.reordered, true, kept); + for (auto& object : kept) recorder->Keep(std::move(object)); + } APS5_LOG_OUT_DEBUG("Beginning pipeline renderExtent=%ux%u", state.renderExtent.width, state.renderExtent.height); record.pipeline->Begin(commands, *record.framebuffer, state.renderExtent, state); } @@ -2026,12 +2065,22 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam auto checkRecords = indirectRecordCheck(record.indirect); APS5_LOG_CHARS_OUT_DEBUG("Draw recorded"); std::function storeProxies; - if (!continued && !record.proxies.empty()) { - storeProxies = [proxies = record.proxies](VkCommandBuffer passCommands) { + if (!continued && (!record.proxies.empty() || !record.reordered.empty() || record.depthLocations)) { + storeProxies = [proxies = record.proxies, reordered = record.reordered, reorderPass = record.reorderPass, depthLocations = record.depthLocations, context, recorder](VkCommandBuffer passCommands) { for (const auto& proxy : proxies) { proxy->RecordAttachmentProxyStore(passCommands, VK_IMAGE_LAYOUT_GENERAL); Recorder::CountBarriers(Recorder::CommandClass::Draw, 2); } + if (!reordered.empty()) { + std::vector> kept; + RecordSampleOrder(context, passCommands, reorderPass, reordered, false, kept); + for (auto& object : kept) recorder->Keep(std::move(object)); + } + if (depthLocations) { + RecordDepthSampleLocations(context, passCommands, *depthLocations); + Recorder::CountBarriers(Recorder::CommandClass::Draw, 2); + recorder->Keep(depthLocations->keep); + } }; } recorder->LeaveRenderPassOpen(passKey, drawTiming, fenced, passAccess, std::move(storeProxies)); @@ -2063,6 +2112,14 @@ std::optional maskedState(const State& state, std::uint32_t fragmentOutpu return masked; } +std::optional passDepthLocations(const Context& context, const State& state, const Pipeline& pipeline) { + if (!state.depth || state.depth->samples <= 1u || !state.customSampleLocations || !pipeline.SampleLocationsEnabled()) return std::nullopt; + auto locations = DepthSurfaceSampleLocations(context, *state.depth); + locations.grid = pipeline.HostSampleLocationGrid(); + locations.locations = SampleLocations(state); + return locations; +} + // Debug aid: APS5_DUMP_TARGETS= saves the first n renders of every color target as a raw file // (u32 width, u32 height, u32 VkFormat, then tightly packed rows). int DumpTargetLimit() { @@ -2103,6 +2160,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"); @@ -2179,6 +2240,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& } APS5_LOG_OUT_DEBUG("Creating color target %zu address=0x%llx bytes=%llu extent=%ux%u", index, static_cast(color.address), static_cast(color.bytes), color.extent.width, color.extent.height); const ColorTargetLayout colorLayout(color.extent.width, color.extent.height, color.tileMode, color.elementBytes); + static_cast(FlushDepthPlanes(color.address, colorLayout.Bytes())); RetireDepthSurfaces(context.device, color.address, colorLayout.Bytes()); constexpr VkBufferUsageFlags copies = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; timer.phase(PhaseSetup); @@ -2197,7 +2259,11 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& if (color.arrayLastSlice != 0) { Require(color.dccAddress == 0 && color.cmaskAddress == 0 && AttachmentProxyFormat(context, color.format) == VK_FORMAT_UNDEFINED, "a slice of a 2D array color target with DCC, CMASK or an attachment proxy has no resident array image"); const auto array = ArraySurfaceForTarget(color); - Require(color.surfaceAddress == color.arrayAddress + color.arraySlice * DescribeSurface(array).layerBytes, "the array image's layer pitch differs from the color view's slice offset"); + // DecodeColorBuffer offsets slice s by s times this size; every slice the image stores must agree. + const auto sliceBytes = ComputeSurfaceSize(ComputeElementMipLayout(ColorTextureTileMode(color.tileMode), color.elementBytes, color.surfaceExtent.width, color.surfaceExtent.height, color.mipCount), 1); + const auto geometry = DescribeSurface(array); + for (std::uint32_t slice = 0; slice <= color.arrayLastSlice; ++slice) Require(geometry.GuestLayerOffset(slice) == slice * sliceBytes, "the array image's layer pitch differs from the color view's slice offset"); + Require(color.surfaceAddress == color.arrayAddress + color.arraySlice * sliceBytes, "the array image's layer pitch differs from the color view's slice offset"); auto resident = CachedStorageSurface(context, array); Require(resident->Attachable(), "storage format cannot be a color attachment"); return resident; @@ -2217,6 +2283,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& materializeCmaskClear(context, color, nullptr); Require(!color.mipTail || binding.gpuTiling, "rendering into a packed mip tail requires the GPU tiled fallback"); Require(color.depth == 1, "rendering into a 3D color target needs the resident image of its surface"); + Require(color.layers == 1, "layered rendering into a 2D array color target needs the resident image of its array"); Require(color.arraySlice == 0 || !ColorTileModeIsXor(color.tileMode), "rendering into a slice of a 2D array color target with an XOR tile mode needs the resident image of its array"); if (binding.gpuTiling) { binding.mip = color.mipTail ? ComputeColorTargetMipLayout(color.tileMode, color.elementBytes, color.surfaceExtent.width, color.surfaceExtent.height, color.mipCount, color.mip) : ColorTargetMip(color, colorLayout); @@ -2250,7 +2317,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& binding.target = std::make_unique(context, color, state.blends.at(color.exportIndex).blendEnable != 0); targetViews.push_back(binding.target->View()); } - if (state.depth) targetViews.push_back(DepthSurfaceView(context, *state.depth)); + if (state.depth) targetViews.push_back(DepthSurfaceView(context, *state.depth, state.depthWrite)); timer.phase(PhasePrepare); const bool recordDraws = RecordDraws(); auto* recorder = Recorder::Active(); @@ -2440,6 +2507,17 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& framebufferOwners.push_back(binding.multisampled ? binding.multisampleOwner : binding.resident); } auto framebuffer = pipeline->AcquireFramebuffer(targetViews, framebufferOwners, state.renderExtent, state.renderLayers); + std::vector reordered; + SampleOrderPass reorderPass; + if (pipeline->ReordersSamples()) { + reorderPass = SampleOrderPassFor(context, pipelineState, pipeline->HostSampleOrder()); + for (const auto& step : PlanSampleOrder(context, state, pipeline->HostSampleOrder())) { + if (step.attachment < targets.size()) reordered.push_back(MultisampleTargetOrderTarget(context, targets[step.attachment].color)); + else reordered.push_back(DepthSurfaceOrderTarget(context, *state.depth)); + Require(reordered.back().method == step.method, "a reordered attachment's surface disagrees with the planned way of putting its samples in order"); + } + } + const auto depthLocations = passDepthLocations(context, pipelineState, *pipeline); timer.phase(PhasePipeline); APS5_LOG_CHARS_OUT_DEBUG("Pipeline created"); if (lean) { @@ -2451,7 +2529,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& record.framebuffer = framebuffer; record.targetViews = targetViews; record.targets = owners; - for (const auto& binding : targets) record.targetSlices.push_back(binding.color.arraySlice); + for (const auto& binding : targets) record.targetSlices.emplace_back(binding.color.arraySlice, binding.color.arrayLastSlice != 0 ? binding.color.layers : 1u); for (const auto& binding : targets) { if (binding.proxied) record.proxies.push_back(binding.resident); if (binding.multisampled) { @@ -2464,12 +2542,15 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& record.completion = completion; record.waited = outcome.waited; record.stencilReplay = stencilReplay; + record.reordered = reordered; + record.reorderPass = reorderPass; + record.depthLocations = depthLocations; recordDraw(context, state, draw, shaders, inputs, record, outcome, timer, ownWaitedMs); // The recipe for the caller's draw-cache entry (design_cpu_final M8, rule R3 for draws): // only a template the resource cache serves under this content key (reusable: no lease, // no copied writes, every direct region import- or mirror-served) of a direct draw, so a // hit's proof is the template's ProveCurrent and nothing needs completion work. - if (recipeOut != nullptr && DrawRecipes() && cacheable && !outcome.waited && args == nullptr && resources->Reusable() && resolved.moved.empty() && !multisampled) { + if (recipeOut != nullptr && DrawRecipes() && cacheable && !outcome.waited && args == nullptr && resources->Reusable() && resolved.moved.empty() && !multisampled && !pipeline->ReordersSamples()) { auto recipe = std::make_shared(); recipe->device = context.device; recipe->templateRef = resources; @@ -2569,7 +2650,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& continue; } if (binding.resident != nullptr) { - imageBarrier(context, commands, binding.resident->Image(), VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT); + imageBarrier(context, commands, binding.resident->Image(), VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, {VK_IMAGE_ASPECT_COLOR_BIT, binding.color.mip, 1, binding.resident->ImageDepth() == 1 ? binding.color.arraySlice : 0, binding.resident->ImageDepth() == 1 ? binding.color.layers : 1}); countBarrier(); continue; } @@ -2588,6 +2669,8 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& context.Resolved(&DeviceFunctions::cmdCopyBufferToImage, "vkCmdCopyBufferToImage")(commands, linear, binding.target->Image(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©); imageBarrier(context, commands, binding.target->Image(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT); } + std::vector> reorderKept; + RecordSampleOrder(context, commands, reorderPass, reordered, true, reorderKept); APS5_LOG_OUT_DEBUG("Beginning pipeline renderExtent=%ux%u", state.renderExtent.width, state.renderExtent.height); pipeline->Begin(commands, *framebuffer, state.renderExtent, state); APS5_LOG_CHARS_OUT_DEBUG("Pipeline Begin OK"); @@ -2614,6 +2697,15 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& APS5_LOG_CHARS_OUT_DEBUG("Draw recorded"); if (recorded) recorder->EndPassSamples(); EndRenderPass(context, commands); + RecordSampleOrder(context, commands, reorderPass, reordered, false, reorderKept); + if (depthLocations) { + RecordDepthSampleLocations(context, commands, *depthLocations); + countBarrier(2); + reorderKept.push_back(depthLocations->keep); + } + if (recorded) { + for (auto& object : reorderKept) recorder->Keep(std::move(object)); + } APS5_LOG_CHARS_OUT_DEBUG("Render pass ended"); for (auto& binding : targets) { if (binding.proxied) { @@ -2622,7 +2714,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& continue; } if (binding.resident != nullptr) { - imageBarrier(context, commands, binding.resident->Image(), VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT); + imageBarrier(context, commands, binding.resident->Image(), VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT, {VK_IMAGE_ASPECT_COLOR_BIT, binding.color.mip, 1, binding.resident->ImageDepth() == 1 ? binding.color.arraySlice : 0, binding.resident->ImageDepth() == 1 ? binding.color.layers : 1}); countBarrier(); continue; } @@ -2665,7 +2757,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& APS5_LOG_CHARS_OUT_DEBUG("SubmitAndWait"); timer.phase(PhaseRecord); if (recorded) { - keepRecordedDraw(*recorder, resources, pipeline, framebuffer, inputs, owners, [&] { std::vector slices; for (const auto& binding : targets) slices.push_back(binding.color.arraySlice); return slices; }(), std::move(scratch), std::move(checkRecords), listed, completion, outcome); + keepRecordedDraw(*recorder, resources, pipeline, framebuffer, inputs, owners, [&] { std::vector> slices; for (const auto& binding : targets) slices.emplace_back(binding.color.arraySlice, binding.color.arrayLastSlice != 0 ? binding.color.layers : 1u); return slices; }(), std::move(scratch), std::move(checkRecords), listed, completion, outcome); timer.phase(PhaseKeep); if (outcome.waited) { // The wait the dispatch path makes for such work (source 3, "address-based"): the @@ -2711,7 +2803,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& if (skipTargetWrite) break; if (binding.resident != nullptr) { // The results stay on the GPU until something reads the target's memory. - binding.resident->MarkSliceDirty(binding.color.arraySlice); + binding.resident->MarkSliceDirty(binding.color.arraySlice, binding.color.arrayLastSlice != 0 ? binding.color.layers : 1u); continue; } if (binding.gpuTiling) GuestMemory::WriteChanged(binding.color.address, binding.tiled->Bytes(), binding.original); @@ -2805,7 +2897,7 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con } if (recipe.hasDepth) { timer.phase(PhaseSetup); - const auto depthView = DepthSurfaceView(context, *state.depth); + const auto depthView = DepthSurfaceView(context, *state.depth, state.depthWrite); timer.phase(PhaseReadTarget); if (recipe.targetViews.back() != depthView) return miss(DrawRecipeMiss::TargetGone); } @@ -2854,9 +2946,10 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con record.framebuffer = std::move(framebuffer); record.targetViews = recipe.targetViews; record.targets = std::move(targets); - for (const auto& color : state.colors) record.targetSlices.push_back(color.arraySlice); + for (const auto& color : state.colors) record.targetSlices.emplace_back(color.arraySlice, color.arrayLastSlice != 0 ? color.layers : 1u); record.pushBytes = &recipe.pushBytes; record.pushStages = recipe.pushStages; + record.depthLocations = passDepthLocations(context, state, *record.pipeline); recordDraw(context, state, draw, shaders, inputs, record, outcome, timer, ownWaitedMs); if (profile) { result.recordUs = std::chrono::duration(std::chrono::steady_clock::now() - recordStart).count(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GpuColorTransfer.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GpuColorTransfer.cpp index 973aff1e34..9881ba2473 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GpuColorTransfer.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GpuColorTransfer.cpp @@ -28,10 +28,7 @@ GpuColorTransfer::GpuColorTransfer(const Context& context) : context(context) { layoutInfo.pushConstantRangeCount = 1; layoutInfo.pPushConstantRanges = &push; Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &pipelineLayout), "vkCreatePipelineLayout color transfer"); - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = sizeof(COLOR_TRANSFER_SPV); - moduleInfo.pCode = COLOR_TRANSFER_SPV; - Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &module), "vkCreateShaderModule color transfer"); + module = CreateShaderModule(context, COLOR_TRANSFER_SPV, sizeof(COLOR_TRANSFER_SPV), "vkCreateShaderModule color transfer"); VkComputePipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; pipelineInfo.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; pipelineInfo.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 880056417d..610c437369 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "ThreadOwned.hpp" #include "prx/libSceAgcDriver/Graphics/include/BdaResources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" @@ -33,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -161,6 +163,29 @@ struct ImportFailure { GuestAllocations::Mapped ranges; }; +struct ImportRefusal { + std::uint64_t base; + std::uint64_t bytes; + GuestAllocations::Mapped ranges; + std::uint64_t liveBytes; + std::uint64_t budget; +}; + +struct EvictingRefusal { + std::uint64_t base; + std::uint64_t bytes; + GuestAllocations::Mapped ranges; + std::uint64_t epoch; + std::uint64_t liveBytes; + std::uint64_t budget; + std::uint64_t room; + std::uint64_t window; + std::uint64_t floor; + std::uint64_t ready; + std::uint64_t readyHeld; + bool joined; +}; + #ifdef _WIN32 constexpr std::size_t settleBatchCount = 4; constexpr std::uint32_t settleBatchCopies = 256; @@ -214,7 +239,9 @@ struct HostImports { std::map failedJoins; std::map sparseRunEnds; std::map sparseRunJoins; - std::set unaliasedSpans; + std::map, ImportFailure> failedBlocks; + std::map, ImportRefusal> refusals; + std::map, EvictingRefusal> evictingRefusals; // Registry generation the imports were last reconciled with. std::uint64_t refreshedGeneration = 0; // Bumped whenever an import is dropped, so HostImport pointers taken under the lock earlier can be @@ -239,6 +266,18 @@ struct HostImports { #endif std::uint64_t useCounter = 0; std::uint64_t buildFloor = 0; + std::array counts{}; + VkPhysicalDevice reportedDevice = VK_NULL_HANDLE; + std::chrono::steady_clock::time_point reportedAt{}; + std::uint64_t reportedBudget = 0; + std::uint32_t reportedHeap = 0; + std::uint64_t reportedHeapBudget = 0; + std::uint64_t reportedHeapUsage = 0; + std::uint64_t ceiling = 0; + std::uint32_t allocationFailures = 0; + bool allocationFailed = false; + std::uint8_t reportNoted = 0; + std::map spaceUses; }; HostImports& Imports() { @@ -489,7 +528,7 @@ VkResult createImportBuffer(const Context& context, VkExternalMemoryHandleTypeFl const char* bindImport(const Context& context, HostImport& entry, VkExternalMemoryHandleTypeFlagBits handleType, const void* import, std::uint32_t importTypes, bool& allocated, VkResult& failure) { const auto failed = [&](const char* step, VkResult result) -> const char* { if (entry.buffer) context.Function("vkDestroyBuffer")(context.device, entry.buffer, nullptr); - if (entry.memory) context.Function("vkFreeMemory")(context.device, entry.memory, nullptr); + if (allocated && entry.memory) context.Function("vkFreeMemory")(context.device, entry.memory, nullptr); entry.buffer = VK_NULL_HANDLE; entry.memory = VK_NULL_HANDLE; failure = result; @@ -506,7 +545,9 @@ const char* bindImport(const Context& context, HostImport& entry, VkExternalMemo ImportFamilies families; if (handleType == VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT) shareDmaBufImport(context, info, families, false); #endif - if (const auto result = context.Function("vkCreateBuffer")(context.device, &info, nullptr, &entry.buffer); result != VK_SUCCESS) return failed("vkCreateBuffer", result); + VkBuffer buffer = VK_NULL_HANDLE; + if (const auto result = context.Function("vkCreateBuffer")(context.device, &info, nullptr, &buffer); result != VK_SUCCESS) return failed("vkCreateBuffer", result); + entry.buffer = buffer; VkMemoryRequirements requirements{}; context.Function("vkGetBufferMemoryRequirements")(context.device, entry.buffer, &requirements); const auto types = requirements.memoryTypeBits & importTypes; @@ -1249,30 +1290,77 @@ std::size_t leasedIndexContaining(const GuestAllocations::Lease& lease, std::uin std::atomic importWindow{0}; -ImportRun importRun(const GuestAllocations::Lease& lease, std::uint64_t address) { +ImportRun leasedRun(const GuestAllocations::Lease& lease, std::size_t first, std::size_t last) { + ImportRun run{lease[first]->address, lease[last]->address + lease[last]->bytes - lease[first]->address, {}}; + run.ranges.assign(lease.begin() + static_cast(first), lease.begin() + static_cast(last) + 1); + return run; +} + +template +const Remembered* rememberedRun(const std::map, Remembered>& runs, const GuestAllocations::Lease& lease, std::size_t first, std::size_t last) { + const auto base = lease[first]->address; + const auto found = runs.find({base, lease[last]->address + lease[last]->bytes - base}); + if (found == runs.end() || found->second.ranges.size() != last - first + 1) return nullptr; + for (std::size_t index = first; index <= last; ++index) { + const auto& remembered = found->second.ranges[index - first]; + if (remembered.owner_before(lease[index]) || lease[index].owner_before(remembered)) return nullptr; + } + return &found->second; +} + +bool stillRefused(const HostImports& state, const ImportRefusal* refusal, std::uint64_t budget) { + return refusal != nullptr && budget != 0 && budget <= refusal->budget && state.liveBytes >= refusal->liveBytes; +} + +ImportRun importRun(const GuestAllocations::Lease& lease, std::uint64_t address, std::uint64_t largest = std::numeric_limits::max(), std::uint64_t budget = 0) { const auto index = leasedIndexContaining(lease, address); if (index == lease.size()) return {}; const auto& containing = *lease[index]; + const auto& state = Imports(); + const auto window = importWindow.load(std::memory_order_relaxed); std::uint64_t low = 0; std::uint64_t high = std::numeric_limits::max(); - const auto window = importWindow.load(std::memory_order_relaxed); if (window != 0) { low = address / window * window; high = low + window; - if (containing.address < low || containing.address + containing.bytes > high) { - low = containing.address; - high = containing.address + containing.bytes; - } + if (containing.address < low || containing.address + containing.bytes > high) return leasedRun(lease, index, index); } auto first = index; while (first > 0 && lease[first - 1]->address >= low && continuesRun(*lease[first - 1], *lease[first])) --first; auto last = index; while (last + 1 < lease.size() && lease[last + 1]->address + lease[last + 1]->bytes <= high && continuesRun(*lease[last], *lease[last + 1])) ++last; - if (window != 0 && (lease[first]->address != low || lease[last]->address + lease[last]->bytes != high)) first = last = index; - if (first != last && Imports().unaliasedSpans.contains(lease[first]->address)) return {lease[index]->address, lease[index]->bytes, {lease[index]}}; - ImportRun run{lease[first]->address, lease[last]->address + lease[last]->bytes - lease[first]->address, {}}; - run.ranges.assign(lease.begin() + static_cast(first), lease.begin() + static_cast(last) + 1); - return run; + const auto skipped = [&](std::size_t from, std::size_t to) { return rememberedRun(state.failedBlocks, lease, from, to) != nullptr || stillRefused(state, rememberedRun(state.refusals, lease, from, to), budget); }; + if (window == 0) return first == last || lease[last]->address + lease[last]->bytes - lease[first]->address > largest || skipped(first, last) ? leasedRun(lease, index, index) : leasedRun(lease, first, last); + const auto bounding = [&](std::size_t from, std::size_t to, std::uint64_t value, bool start) { + const auto found = std::lower_bound(lease.begin() + static_cast(from), lease.begin() + static_cast(to) + 1, value, [start](const auto& range, std::uint64_t wanted) { return (start ? range->address : range->address + range->bytes) < wanted; }); + const auto position = static_cast(found - lease.begin()); + return position <= to && (start ? (*found)->address : (*found)->address + (*found)->bytes) == value ? position : lease.size(); + }; + for (auto size = window; size >= containing.bytes; size /= 2) { + if (size > largest) continue; + const auto blockLow = address / size * size; + const auto blockHigh = blockLow + size; + if (containing.address < blockLow || containing.address + containing.bytes > blockHigh) break; + if (blockLow < lease[first]->address || blockHigh > lease[last]->address + lease[last]->bytes) continue; + const auto blockFirst = bounding(first, index, blockLow, true); + const auto blockLast = bounding(index, last, blockHigh, false); + if (blockFirst == lease.size() || blockLast == lease.size()) continue; + if (blockFirst == blockLast) break; + if (skipped(blockFirst, blockLast)) continue; + return leasedRun(lease, blockFirst, blockLast); + } + return leasedRun(lease, index, index); +} + +ImportRun exactRun(const GuestAllocations::Lease& lease, std::uint64_t base, std::uint64_t bytes) { + const auto first = leasedIndexContaining(lease, base); + if (first == lease.size() || lease[first]->address != base) return {}; + auto last = first; + while (lease[last]->address + lease[last]->bytes < base + bytes) { + if (last + 1 == lease.size() || !continuesRun(*lease[last], *lease[last + 1])) return {}; + ++last; + } + return lease[last]->address + lease[last]->bytes == base + bytes ? leasedRun(lease, first, last) : ImportRun{}; } bool runCovers(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end) { @@ -1332,27 +1420,38 @@ bool wouldRetireImport(const HostImports& state, const ImportRun& run) { return previous.base + previous.bytes > run.base; } -bool sameRange(const ImportFailure& entry, const GuestAllocations::Lease& lease) { +bool sameRange(const ImportFailure& entry, const GuestAllocations::Lease& lease, std::uint64_t budget = 0) { const auto current = entry.ranges.size() == 1 ? leasedRangeOwner(lease, entry.base, entry.base + entry.bytes) : nullptr; if (current != nullptr && entry.bytes < current->bytes) return !entry.ranges.front().owner_before(current) && !current.owner_before(entry.ranges.front()); - return sameRun(entry, importRun(lease, entry.base)); + return sameRun(entry, importRun(lease, entry.base, std::numeric_limits::max(), budget)); } -bool sameRange(const HostImport& entry, const GuestAllocations::Lease& lease) { +bool sameRange(const HostImport& entry, const GuestAllocations::Lease& lease, std::uint64_t budget = 0) { if (entry.ranges.size() == 1 && !entry.joined && entry.bytes < entry.ranges.front()->bytes) { const auto current = leasedRangeOwner(lease, entry.base, entry.base + entry.bytes); return current != nullptr && !entry.ranges.front().owner_before(current) && !current.owner_before(entry.ranges.front()); } - return sameRun(entry, entry.joined ? joinedRun(lease, entry.base, entry.base + entry.bytes) : importRun(lease, entry.base)); + return sameRun(entry, entry.joined ? joinedRun(lease, entry.base, entry.base + entry.bytes) : importRun(lease, entry.base, std::numeric_limits::max(), budget)); +} + +std::uint64_t effectiveUse(const HostImports& state, const HostImport& entry) { + if (entry.spaceSerial == 0) return entry.lastUse; + const auto found = state.spaceUses.find(entry.spaceSerial); + return found == state.spaceUses.end() ? entry.lastUse : std::max(entry.lastUse, found->second); } bool pointerHeld(const HostImports& state, const HostImport& entry) { - return entry.lastUse >= state.buildFloor; + return effectiveUse(state, entry) >= state.buildFloor; } +constexpr std::uint64_t ProtectedUses = 256; + bool evictable(const HostImports& state, const HostImport& entry) { - constexpr std::uint64_t protectedUses = 256; - return !pointerHeld(state, entry) && entry.lastUse + protectedUses < state.useCounter; + return !pointerHeld(state, entry) && effectiveUse(state, entry) + ProtectedUses < state.useCounter; +} + +bool evictingRefusalCurrent(const HostImports& state, const EvictingRefusal& refusal, std::uint64_t budget) { + return refusal.epoch == state.epoch && refusal.liveBytes == state.liveBytes && budget <= refusal.budget && refusal.window == importWindow.load(std::memory_order_relaxed) && state.useCounter < refusal.ready && (state.buildFloor <= refusal.floor || state.useCounter < refusal.readyHeld); } std::uint64_t retireLeastRecent(const Context& context, HostImports& state, const GuestAllocations::Lease& lease, std::uint64_t wanted) { @@ -1360,7 +1459,7 @@ std::uint64_t retireLeastRecent(const Context& context, HostImports& state, cons while (freed < wanted) { auto oldest = state.imports.end(); for (auto it = state.imports.begin(); it != state.imports.end(); ++it) { - if (evictable(state, it->second) && (oldest == state.imports.end() || it->second.lastUse < oldest->second.lastUse)) oldest = it; + if (evictable(state, it->second) && (oldest == state.imports.end() || effectiveUse(state, it->second) < effectiveUse(state, oldest->second))) oldest = it; } if (oldest == state.imports.end()) break; freed += oldest->second.pinnedBytes; @@ -1387,16 +1486,70 @@ std::atomic addressCopyLimitOverride{0}; // Pinned imports count against the driver's system memory heap, which imports, type-3 mirrors, buffers and textures // placed in host memory share; past it ordinary host allocations fail. The budget is APS5_HOST_IMPORT_MIB, else // HostImportBudget. -std::uint64_t importBudget(const Context& context) { +std::uint64_t configuredBudget() { if (const auto set = budgetOverride.load(std::memory_order_relaxed); set != 0) return set; static const std::uint64_t configured = []() -> std::uint64_t { const char* value = std::getenv("APS5_HOST_IMPORT_MIB"); return value != nullptr ? std::strtoull(value, nullptr, 10) << 20u : 0u; }(); - if (configured != 0) return configured; + return configured; +} + +std::uint64_t importBudget(const Context& context) { + if (const auto configured = configuredBudget(); configured != 0) return configured; return HostImportBudget(context.memory, physicalMemoryBytes()); } +std::optional hostHeapIndex(const VkPhysicalDeviceMemoryProperties& memory) { + std::optional heap; + for (std::uint32_t index = 0; index < memory.memoryHeapCount; ++index) { + if ((memory.memoryHeaps[index].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) == 0 && (!heap || memory.memoryHeaps[index].size > memory.memoryHeaps[*heap].size)) heap = index; + } + return heap; +} + +void noteReportedBudget(HostImports& state, std::uint64_t configured, bool set, std::uint64_t reported) { + if (reported >= configured || state.reportedHeapBudget == 0) return; + const std::uint8_t stage = !set && state.allocationFailed ? 2 : 1; + if (state.reportNoted >= stage) return; + state.reportNoted = stage; + const char* outcome = set ? "the setting stays, as the report never lowers it" : stage == 2 ? "imports are held to it since an import's vkAllocateMemory failed" : "the default stays until an import's vkAllocateMemory fails"; + std::fprintf(stderr, "[gpu] host import budget: host heap %u reports a VK_EXT_memory_budget budget of %llu MiB and a usage of %llu MiB with %llu MiB of imports live, which leaves imports %llu MiB of the %llu MiB %s; %s\n", state.reportedHeap, static_cast(state.reportedHeapBudget >> 20u), static_cast(state.reportedHeapUsage >> 20u), static_cast(state.liveBytes >> 20u), static_cast(reported >> 20u), static_cast(configured >> 20u), set ? "APS5_HOST_IMPORT_MIB sets" : "of the default", outcome); +} + +std::uint64_t effectiveBudget(const Context& context, HostImports& state) { + const auto configured = importBudget(context); + const bool set = configuredBudget() != 0; + auto reported = std::numeric_limits::max(); + if (context.memoryProperties2 != nullptr && context.physical != VK_NULL_HANDLE) { + const auto now = std::chrono::steady_clock::now(); + if (state.reportedDevice != context.physical || now - state.reportedAt >= std::chrono::seconds(1)) { + VkPhysicalDeviceMemoryBudgetPropertiesEXT report{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT}; + VkPhysicalDeviceMemoryProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2, &report}; + context.memoryProperties2(context.physical, &properties); + state.reportedBudget = HostImportBudgetFor(context.memory, &report, std::numeric_limits::max(), state.liveBytes); + const auto heap = hostHeapIndex(context.memory); + state.reportedHeap = heap.value_or(0); + state.reportedHeapBudget = heap ? report.heapBudget[*heap] : 0; + state.reportedHeapUsage = heap ? report.heapUsage[*heap] : 0; + state.reportedDevice = context.physical; + state.reportedAt = now; + } + reported = state.reportedBudget; + } + noteReportedBudget(state, configured, set, reported); + return HostImportBudgetWith(configured, set, reported, state.allocationFailed, state.ceiling); +} + +std::string budgetSource(const Context& context, const HostImports& state, std::uint64_t budget) { + const auto configured = importBudget(context); + const char* source = state.ceiling != 0 && budget == state.ceiling ? "lowered after vkAllocateMemory failed" : budget < configured ? "the host heap's VK_EXT_memory_budget report" : configuredBudget() != 0 ? "APS5_HOST_IMPORT_MIB" : "the default"; + if (state.reportedHeapBudget == 0) return source; + char text[192]; + std::snprintf(text, sizeof(text), "%s; host heap %u reports a budget of %llu MiB and a usage of %llu MiB", source, state.reportedHeap, static_cast(state.reportedHeapBudget >> 20u), static_cast(state.reportedHeapUsage >> 20u)); + return text; +} + std::atomic rangeImportWindow{64ull << 20u}; // The import of an access to a registered range larger than the range window: the cells the access touches, grown to @@ -1432,13 +1585,19 @@ ImportRun windowRun(const HostImports& state, ImportRun run, std::uint64_t begin const HostImport* peekImport(const HostImports& state, std::uint64_t begin, std::uint64_t end); const HostImport* findImport(HostImports& state, std::uint64_t begin, std::uint64_t end); -const HostImport* importRunAt(const Context& context, HostImports& state, ImportRun run, bool joined, std::uint64_t address, const GuestAllocations::Lease& lease, std::uint64_t end, bool evict); +const HostImport* importRunAt(const Context& context, HostImports& state, ImportRun run, bool joined, std::uint64_t address, const GuestAllocations::Lease& lease, std::uint64_t end, bool evict, ImportRequester requester); // The bytes retireLeastRecent could free outside [begin, end). -std::uint64_t evictableBytes(const HostImports& state, std::uint64_t begin, std::uint64_t end) { +std::uint64_t evictableBytes(const HostImports& state, std::uint64_t begin, std::uint64_t end, std::uint64_t& ready, std::uint64_t& readyHeld) { std::uint64_t bytes = 0; for (const auto& [base, entry] : state.imports) { - if ((entry.base + entry.bytes <= begin || entry.base >= end) && evictable(state, entry)) bytes += entry.pinnedBytes; + if (entry.base + entry.bytes > begin && entry.base < end) continue; + if (evictable(state, entry)) { + bytes += entry.pinnedBytes; + continue; + } + auto& horizon = pointerHeld(state, entry) ? readyHeld : ready; + horizon = std::min(horizon, effectiveUse(state, entry) + ProtectedUses + 1); } return bytes; } @@ -1465,7 +1624,7 @@ struct ImportConstruction { void ensurePinWaiter(); -const HostImport* importAllocation(const Context& context, HostImports& state, std::uint64_t address, const GuestAllocations::Lease& lease, std::uint64_t end = 0, bool evict = true) { +const HostImport* importAllocation(const Context& context, HostImports& state, std::uint64_t address, const GuestAllocations::Lease& lease, ImportRequester requester, std::uint64_t end = 0, bool evict = true, std::uint64_t largest = std::numeric_limits::max()) { ensurePinWaiter(); #ifdef _WIN32 settleUnwatchedImports(context, state); @@ -1479,7 +1638,15 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s return current; } } - auto run = importRun(lease, address); + const auto budget = evict ? 0 : effectiveBudget(context, state); + if (const auto index = leasedIndexContaining(lease, address); budget != 0 && index != lease.size() && stillRefused(state, rememberedRun(state.refusals, lease, index, index), budget) && findImport(state, address, address + 1) == nullptr) { + auto& counts = state.counts[static_cast(requester)]; + ++counts.refused; + counts.refusedBytes += lease[index]->bytes; + ++counts.remembered; + return nullptr; + } + auto run = importRun(lease, address, largest, budget); // Without an end the caller views the whole run through the import (AcquireRegistered): a window that holds the // first byte is shorter than that, so only an import of the whole run is a hit; a window is replaced below. if (fresh && end == 0) { @@ -1487,7 +1654,7 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s } if (end != 0) run = windowRun(state, std::move(run), address, end, context.hostImportAlignment); if (run.ranges.empty()) return nullptr; - return importRunAt(context, state, std::move(run), false, address, lease, end, evict); + return importRunAt(context, state, std::move(run), false, address, lease, end, evict, requester); } const HostImport* importJoined(const Context& context, HostImports& state, std::uint64_t begin, std::uint64_t end, const GuestAllocations::Lease& lease) { @@ -1496,13 +1663,15 @@ const HostImport* importJoined(const Context& context, HostImports& state, std:: auto run = joinedRun(lease, begin, end); if (run.ranges.empty()) return nullptr; if (const auto failed = state.failedJoins.find(run.base); failed != state.failedJoins.end() && sameRun(failed->second, run)) return nullptr; - return importRunAt(context, state, std::move(run), true, begin, lease, end, true); + return importRunAt(context, state, std::move(run), true, begin, lease, end, true, ImportRequester::Join); } -const HostImport* importRunAt(const Context& context, HostImports& state, ImportRun run, bool joined, std::uint64_t address, const GuestAllocations::Lease& lease, std::uint64_t end, bool evict) { +const HostImport* importRunAt(const Context& context, HostImports& state, ImportRun run, bool joined, std::uint64_t address, const GuestAllocations::Lease& lease, std::uint64_t end, bool evict, ImportRequester requester) { + auto& counts = state.counts[static_cast(requester)]; const auto base = run.base; const auto bytes = run.bytes; auto& failures = joined ? state.failedJoins : state.failed; + const bool stepDown = !joined && run.ranges.size() > 1 && importWindow.load(std::memory_order_relaxed) != 0; const auto alignment = context.hostImportAlignment; if (joined && (alignment == 0 || base % alignment != 0 || bytes % alignment != 0)) { failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(run.ranges.begin(), run.ranges.end())}); @@ -1513,11 +1682,24 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import return &found->second; } if (alignment == 0 || base % alignment != 0 || bytes % alignment != 0) return nullptr; - if (const auto failed = failures.find(base); failed != failures.end()) { + if (const auto failed = failures.find(base); !stepDown && failed != failures.end()) { if (sameRun(failed->second, run)) return nullptr; failures.erase(failed); } dropTrimmed(state, base, base + bytes); + const auto budget = effectiveBudget(context, state); + const auto containing = std::find_if(run.ranges.begin(), run.ranges.end(), [address](const auto& range) { return address >= range->address && address < range->address + range->bytes; }); + if (const auto remembered = state.evictingRefusals.find({base, bytes}); evict && remembered != state.evictingRefusals.end()) { + if (remembered->second.joined == joined && evictingRefusalCurrent(state, remembered->second, budget) && sameRun(remembered->second, run)) { + ++counts.refused; + counts.refusedBytes += bytes; + ++counts.remembered; + const auto room = remembered->second.room - std::min(remembered->second.room, remembered->second.budget - budget); + if (stepDown && containing != run.ranges.end() && room >= (*containing)->bytes) return importAllocation(context, state, address, lease, requester, end, evict, std::min(bytes / 2, room)); + return nullptr; + } + state.evictingRefusals.erase(remembered); + } auto first = state.imports.lower_bound(base); if (first != state.imports.begin() && std::prev(first)->second.base + std::prev(first)->second.bytes > base) --first; std::uint64_t replaced = 0; @@ -1530,21 +1712,38 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import return !expandedWindow || Recorder::Active() == nullptr; }; for (auto it = first; it != state.imports.end() && it->first < base + bytes; ++it) { - if (held(it->second)) return nullptr; + if (held(it->second)) return stepDown ? importAllocation(context, state, address, lease, requester, end, evict, bytes / 2) : nullptr; replaced += it->second.pinnedBytes; } - const auto budget = importBudget(context); const auto live = state.liveBytes - replaced; - if (live + bytes > budget && (!evict || live + bytes - evictableBytes(state, base, base + bytes) > budget)) { + auto ready = std::numeric_limits::max(); + auto readyHeld = std::numeric_limits::max(); + const auto evictable = evict && live + bytes > budget ? evictableBytes(state, base, base + bytes, ready, readyHeld) : 0; + if (live + bytes > budget + evictable) { // A refused import turns every later use of the range into CPU copies, so say so. - static std::uint64_t refused = 0, refusedBytes = 0; static auto lastReport = std::chrono::steady_clock::now() - std::chrono::seconds(60); - ++refused; - refusedBytes += bytes; + ++counts.refused; + counts.refusedBytes += bytes; + const auto remember = [&](std::uint64_t at, std::uint64_t size, GuestAllocations::Mapped ranges) { + if (!evict && !joined) state.refusals.insert_or_assign(std::pair{at, size}, ImportRefusal{at, size, std::move(ranges), state.liveBytes, budget}); + }; + remember(base, bytes, GuestAllocations::Mapped(run.ranges.begin(), run.ranges.end())); if (std::chrono::steady_clock::now() - lastReport > std::chrono::seconds(10)) { lastReport = std::chrono::steady_clock::now(); - std::fprintf(stderr, "[gpu] host import budget: %llu MiB live of %llu MiB (APS5_HOST_IMPORT_MIB); %llu imports (%llu MiB) refused so far, last 0x%llx+0x%llx\n", static_cast(state.liveBytes >> 20u), static_cast(budget >> 20u), static_cast(refused), static_cast(refusedBytes >> 20u), static_cast(base), static_cast(bytes)); + std::uint64_t refused = 0; + std::uint64_t refusedBytes = 0; + std::uint64_t remembered = 0; + for (const auto& requested : state.counts) { + refused += requested.refused; + refusedBytes += requested.refusedBytes; + remembered += requested.remembered; + } + std::fprintf(stderr, "[gpu] host import budget: %llu MiB live of %llu MiB (%s); %llu imports (%llu MiB) refused so far, %llu of them remembered, last 0x%llx+0x%llx by %s\n", static_cast(state.liveBytes >> 20u), static_cast(budget >> 20u), budgetSource(context, state, budget).c_str(), static_cast(refused), static_cast(refusedBytes >> 20u), static_cast(remembered), static_cast(base), static_cast(bytes), ImportRequesterName(requester)); } + const auto room = budget + evictable > live ? budget + evictable - live : 0; + if (evict) state.evictingRefusals.insert_or_assign(std::pair{base, bytes}, EvictingRefusal{base, bytes, GuestAllocations::Mapped(run.ranges.begin(), run.ranges.end()), state.epoch, state.liveBytes, budget, room, importWindow.load(std::memory_order_relaxed), state.buildFloor, ready, readyHeld, joined}); + if (stepDown && containing != run.ranges.end() && room >= (*containing)->bytes) return importAllocation(context, state, address, lease, requester, end, evict, std::min(bytes / 2, room)); + if (containing != run.ranges.end() && run.ranges.size() > 1) remember((*containing)->address, (*containing)->bytes, GuestAllocations::Mapped{*containing}); return nullptr; } for (auto it = first; it != state.imports.end() && it->first < base + bytes;) { @@ -1560,17 +1759,26 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import entry.memoryBase = base; entry.pinnedBytes = bytes; entry.backingSerial = entry.ranges.size() == 1 ? entry.range->backingSerial : 0; + const auto failedBlockImport = [&]() -> const HostImport* { + ++counts.failed; + state.failedBlocks.insert_or_assign(std::pair{base, bytes}, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())}); + return importAllocation(context, state, address, lease, requester, end, evict); + }; + const auto failedImport = [&]() -> const HostImport* { + if (stepDown) return failedBlockImport(); + ++counts.failed; + failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())}); + return nullptr; + }; #ifdef _WIN32 // Drivers pin imported pages, so every page must be committed and accessible. + const GuestArena::HostImportAccess importAccess(reinterpret_cast(base), static_cast(bytes)); bool writable = true; bool readOnly = true; MEMORY_BASIC_INFORMATION refused{}; for (std::uint64_t cursor = base; cursor < base + bytes;) { MEMORY_BASIC_INFORMATION info{}; - if (VirtualQuery(reinterpret_cast(cursor), &info, sizeof(info)) == 0 || info.State != MEM_COMMIT || (info.Protect & (PAGE_NOACCESS | PAGE_GUARD)) != 0) { - failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())}); - return nullptr; - } + if (VirtualQuery(reinterpret_cast(cursor), &info, sizeof(info)) == 0 || info.State != MEM_COMMIT || (info.Protect & (PAGE_NOACCESS | PAGE_GUARD)) != 0) return failedImport(); const auto protection = info.Protect & 0xffu; const bool pageWritable = (info.Type == MEM_PRIVATE || info.Type == MEM_IMAGE) && (protection == PAGE_READWRITE || protection == PAGE_EXECUTE_READWRITE || protection == PAGE_WRITECOPY || protection == PAGE_EXECUTE_WRITECOPY); const bool pageReadOnly = info.Type != MEM_MAPPED && (protection == PAGE_READONLY || protection == PAGE_EXECUTE_READ); @@ -1579,10 +1787,7 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import if (!pageWritable) writable = false; cursor = reinterpret_cast(info.BaseAddress) + info.RegionSize; } - if (!writable && readOnly) { - failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())}); - return nullptr; - } + if (!writable && readOnly) return failedImport(); if (!writable) { if (refused.BaseAddress != nullptr) { char text[256]; @@ -1593,20 +1798,12 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import entry.alias = GuestArena::GuestArenaMapAlias_nid_postfix(static_cast(base), static_cast(bytes)); } catch (const std::runtime_error&) { if (entry.ranges.size() == 1) throw; - if (joined) { - failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())}); - return nullptr; - } - state.unaliasedSpans.insert(base); - return importAllocation(context, state, address, lease, end, evict); + return joined ? failedImport() : failedBlockImport(); } if (entry.alias == nullptr) return nullptr; } #else - if (!GuestMemory::Accessible(reinterpret_cast(base), static_cast(bytes), true)) { - failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())}); - return nullptr; - } + if (!GuestMemory::Accessible(reinterpret_cast(base), static_cast(bytes), true)) return failedImport(); #endif decideImportWatch(context, state); VkResult result = VK_SUCCESS; @@ -1652,8 +1849,17 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import #endif entry.unwatched = step == nullptr && !(entry.dmaBuf ? watchedDmaBuf : watchedHostPointer); if (step != nullptr) { - failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())}); - std::fprintf(stderr, "[gpu] host import of 0x%llx+0x%llx failed at %s (%d); falling back to copies\n", static_cast(base), static_cast(bytes), step, static_cast(result)); +#ifdef _WIN32 + GuestArena::GuestArenaUnmapAlias_nid_postfix(std::exchange(entry.alias, nullptr)); +#endif + std::fprintf(stderr, "[gpu] host import of 0x%llx+0x%llx failed at %s (%d); falling back to copies, requested by %s\n", static_cast(base), static_cast(bytes), step, static_cast(result), ImportRequesterName(requester)); + if (std::string_view(step) == "vkAllocateMemory") { + state.allocationFailed = true; + if (++state.allocationFailures >= 3 || state.liveBytes + bytes + budget / 8 >= budget) { + state.ceiling = std::max(state.liveBytes, 1); + std::fprintf(stderr, "[gpu] host import budget lowered to the %.1f MiB live after vkAllocateMemory failed %s\n", state.liveBytes / 1048576.0, state.allocationFailures >= 3 ? "3 times in a row" : "within an eighth of the budget"); + } + } #ifdef _WIN32 static const bool trace = std::getenv("APS5_TRACE_HOST_IMPORT") != nullptr; for (std::uint64_t cursor = base; trace && cursor < base + bytes;) { @@ -1676,13 +1882,17 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import std::fclose(maps); } #endif - return nullptr; + return failedImport(); } static std::uint64_t importedBytes = 0; importedBytes += bytes; static const bool trace = std::getenv("APS5_TRACE_HOST_IMPORT") != nullptr; state.liveBytes += bytes; - if (trace) std::fprintf(stderr, "[gpu] host import of 0x%llx+0x%llx ok (%zu live, %.1f MiB live, %.1f MiB ever)%s\n", static_cast(base), static_cast(bytes), state.imports.size() + 1, state.liveBytes / 1048576.0, importedBytes / 1048576.0, joined ? ", joined at bind" : ""); + state.allocationFailures = 0; + ++counts.made; + counts.madeBytes += bytes; + for (auto it = state.refusals.lower_bound({base, 0}); it != state.refusals.end() && it->first.first < base + bytes;) it = it->first.first + it->first.second <= base + bytes ? state.refusals.erase(it) : std::next(it); + if (trace) std::fprintf(stderr, "[gpu] host import of 0x%llx+0x%llx ok (%zu live, %.1f MiB live, %.1f MiB ever)%s by %s\n", static_cast(base), static_cast(bytes), state.imports.size() + 1, state.liveBytes / 1048576.0, importedBytes / 1048576.0, joined ? ", joined at bind" : "", ImportRequesterName(requester)); entry.joined = joined; entry.lastUse = ++state.useCounter; #ifdef _WIN32 @@ -1772,6 +1982,13 @@ void refreshImports(const Context& context, HostImports& state, const GuestAlloc state.liveBytes = 0; state.failed.clear(); state.failedJoins.clear(); + state.failedBlocks.clear(); + state.refusals.clear(); + state.evictingRefusals.clear(); + state.ceiling = 0; + state.allocationFailures = 0; + state.allocationFailed = false; + state.reportNoted = 0; state.sparseRunEnds.clear(); state.sparseRunJoins.clear(); state.device = context.device; @@ -1801,8 +2018,9 @@ void refreshImports(const Context& context, HostImports& state, const GuestAlloc ++state.epoch; state.imports.insert(std::move(node)); } + const auto budget = effectiveBudget(context, state); for (auto it = state.imports.begin(); it != state.imports.end();) { - if (sameRange(it->second, lease)) { + if (sameRange(it->second, lease, budget)) { if (it->second.unwatched) GuestMemory::Unwatch(it->first, it->second.bytes); ++it; continue; @@ -1813,8 +2031,11 @@ void refreshImports(const Context& context, HostImports& state, const GuestAlloc retireImport(context.device, state, it, lease); it = next; } - for (auto it = state.failed.begin(); it != state.failed.end();) it = sameRange(it->second, lease) ? std::next(it) : state.failed.erase(it); + for (auto it = state.failed.begin(); it != state.failed.end();) it = sameRange(it->second, lease, budget) ? std::next(it) : state.failed.erase(it); for (auto it = state.failedJoins.begin(); it != state.failedJoins.end();) it = sameRun(it->second, joinedRun(lease, it->second.base, it->second.base + it->second.bytes)) ? std::next(it) : state.failedJoins.erase(it); + for (auto it = state.failedBlocks.begin(); it != state.failedBlocks.end();) it = sameRun(it->second, exactRun(lease, it->first.first, it->first.second)) ? std::next(it) : state.failedBlocks.erase(it); + for (auto it = state.refusals.begin(); it != state.refusals.end();) it = stillRefused(state, &it->second, budget) && sameRun(it->second, exactRun(lease, it->first.first, it->first.second)) ? std::next(it) : state.refusals.erase(it); + for (auto it = state.evictingRefusals.begin(); it != state.evictingRefusals.end();) it = evictingRefusalCurrent(state, it->second, budget) && sameRun(it->second, exactRun(lease, it->first.first, it->first.second)) ? std::next(it) : state.evictingRefusals.erase(it); } const HostImport* peekImport(const HostImports& state, std::uint64_t begin, std::uint64_t end) { @@ -1831,9 +2052,9 @@ const HostImport* findImport(HostImports& state, std::uint64_t begin, std::uint6 return entry; } -const HostImport* importAccess(const Context& context, HostImports& state, std::uint64_t begin, std::uint64_t end, const GuestAllocations::Lease& lease) { +const HostImport* importAccess(const Context& context, HostImports& state, std::uint64_t begin, std::uint64_t end, const GuestAllocations::Lease& lease, ImportRequester requester) { if (!runCovers(lease, begin, end)) return findImport(state, begin, end); - const auto* entry = importAllocation(context, state, begin, lease, end); + const auto* entry = importAllocation(context, state, begin, lease, requester, end); return entry != nullptr && findImport(state, begin, end) == entry ? entry : nullptr; } @@ -2391,7 +2612,7 @@ bool heapMirrorWatched(ImageMirror& mirror) { } bool importsNow(const Context& context, HostImports& state, const GuestAllocations::Lease& lease, std::uint64_t address) { - return context.hostImportAlignment != 0 && (importsStale(context, state) || importAllocation(context, state, address, lease, 0, false) != nullptr); + return context.hostImportAlignment != 0 && (importsStale(context, state) || importAllocation(context, state, address, lease, ImportRequester::Build, 0, false) != nullptr); } // The mirror for a leased image range: the existing one, or a new one when none exists or the @@ -2623,6 +2844,7 @@ LeaseState& Leases() { struct SnapshotStats { std::atomic checked{0}; std::atomic skipped{0}; + std::atomic mismatched{0}; }; SnapshotStats& Snapshots() { @@ -2855,6 +3077,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); @@ -2921,6 +3147,13 @@ void ClearHostImports(VkDevice device) { state.liveBytes = 0; state.failed.clear(); state.failedJoins.clear(); + state.failedBlocks.clear(); + state.refusals.clear(); + state.evictingRefusals.clear(); + state.ceiling = 0; + state.allocationFailures = 0; + state.allocationFailed = false; + state.reportNoted = 0; state.device = VK_NULL_HANDLE; state.refreshedGeneration = 0; ++state.epoch; @@ -3152,6 +3385,27 @@ void SetHostImportRangeWindow(std::uint64_t bytes) { void SetHostImportBudget(std::uint64_t bytes) { budgetOverride.store(bytes, std::memory_order_relaxed); + auto& state = Imports(); + std::lock_guard lock(state.mutex); + state.ceiling = 0; + state.allocationFailures = 0; + state.allocationFailed = false; + state.reportNoted = 0; +} + +std::uint64_t HostImportBudgetFor(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t configured, std::uint64_t liveBytes) { + const auto heap = hostHeapIndex(memory); + if (reported == nullptr || !heap || reported->heapBudget[*heap] == 0) return configured; + const auto granted = reported->heapBudget[*heap]; + const auto others = reported->heapUsage[*heap] - std::min(reported->heapUsage[*heap], liveBytes); + const auto held = others + granted / 8u; + constexpr std::uint64_t step = 64ull << 20u; + return std::min(configured, granted > held ? (granted - held) / step * step : 0u); +} + +std::uint64_t HostImportBudgetWith(std::uint64_t configured, bool set, std::uint64_t reported, bool allocationFailed, std::uint64_t ceiling) { + const auto budget = set || !allocationFailed ? configured : std::max(std::min(configured, reported), 1); + return ceiling != 0 ? std::min(budget, ceiling) : budget; } void SetAddressCopyLimit(std::uint64_t bytes) { @@ -3182,6 +3436,49 @@ std::uint64_t HostImportLiveBytes() { return state.liveBytes; } +const char* ImportRequesterName(ImportRequester requester) { + switch (requester) { + case ImportRequester::Map: return "map"; + case ImportRequester::Build: return "build"; + case ImportRequester::Access: return "access"; + case ImportRequester::Join: return "join"; + case ImportRequester::Segment: return "segment"; + } + return "unknown"; +} + +HostImportCounts HostImportCounters() { + auto& state = Imports(); + std::lock_guard lock(state.mutex); + return {state.imports.size(), state.liveBytes, state.counts}; +} + +std::string FormatHostImportCounts(const HostImportCounts& counts) { + HostImportRequesterCounts total; + std::string requesters; + for (std::size_t index = 0; index < counts.requesters.size(); ++index) { + const auto& requested = counts.requesters[index]; + total.made += requested.made; + total.madeBytes += requested.madeBytes; + total.refused += requested.refused; + total.refusedBytes += requested.refusedBytes; + total.failed += requested.failed; + total.remembered += requested.remembered; + char part[96]; + std::snprintf(part, sizeof(part), "%s%s %llu/%llu/%llu", index == 0 ? "" : ", ", ImportRequesterName(static_cast(index)), static_cast(requested.made), static_cast(requested.refused), static_cast(requested.failed)); + requesters += part; + } + char text[256]; + std::snprintf(text, sizeof(text), "%llu live (%.1f MiB); made %llu (%.1f MiB), refused %llu (%.1f MiB, %llu remembered), failed %llu; by requester (made/refused/failed): ", static_cast(counts.live), counts.liveBytes / 1048576.0, static_cast(total.made), total.madeBytes / 1048576.0, static_cast(total.refused), total.refusedBytes / 1048576.0, static_cast(total.remembered), static_cast(total.failed)); + return text + requesters; +} + +void ReportHostImports() { + const auto counts = HostImportCounters(); + if (std::all_of(counts.requesters.begin(), counts.requesters.end(), [](const HostImportRequesterCounts& requested) { return requested.made == 0 && requested.refused == 0 && requested.failed == 0; })) return; + std::fprintf(stderr, "[gpu] host imports at shutdown: %s\n", FormatHostImportCounts(counts).c_str()); +} + HostImportSettleCounts HostImportSettles() { #ifdef _WIN32 auto& state = Imports(); @@ -3196,7 +3493,7 @@ std::uint64_t HostImportsSettled() { return settledImports.load(std::memory_order_relaxed); } -const HostImport* HostImportFor(const Context& context, std::uint64_t address, std::size_t bytes) { +const HostImport* HostImportFor(const Context& context, std::uint64_t address, std::size_t bytes, ImportRequester requester) { if (context.hostImportAlignment == 0 || bytes == 0 || bytes > std::numeric_limits::max() - address) return nullptr; ensurePinWaiter(); auto& state = Imports(); @@ -3213,7 +3510,7 @@ const HostImport* HostImportFor(const Context& context, std::uint64_t address, s } const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); refreshImports(context, state, lease); - return importAccess(context, state, address, address + bytes, lease); + return importAccess(context, state, address, address + bytes, lease, requester); } const HostImport* ExistingHostImportFor(const Context& context, std::uint64_t address, std::size_t bytes) { @@ -3244,11 +3541,11 @@ bool ImportMappedRanges(const Context& context, const GuestAllocations::Mapped& if (range == nullptr || leasedRangeOwner(current, range->address) != range) continue; try { if (!GuestMemory::GpuMutex().HeldByThisThread()) { - const auto run = importRun(current, range->address); + const auto run = importRun(current, range->address, std::numeric_limits::max(), effectiveBudget(context, state)); const std::array, 1> span{{{run.base, run.base + run.bytes}}}; if (wouldRetireImport(state, run) || StorageTexture::AnyPendingOverlaps(span)) return false; } - if (importAllocation(context, state, range->address, current, 0, false) == nullptr && GuestAllocations::GuestAllocationsGeneration_nid_postfix() != generation) state.failed.erase(importRun(current, range->address).base); + importAllocation(context, state, range->address, current, ImportRequester::Map, 0, false); } catch (const std::exception& error) { std::fprintf(stderr, "[gpu] import of GPU memory mapped at 0x%llx+0x%zx failed: %s\n", static_cast(range->address), range->bytes, error.what()); } @@ -3360,8 +3657,24 @@ void GuestBufferMemory::AcquireRegistered() { importFloor = state.useCounter + 1; if (hit) { const auto stamp = ++state.useCounter; - for (const auto& region : current->base) { - if (region.direct != nullptr) region.direct->lastUse = stamp; + static const bool stampOnce = std::getenv("APS5_NO_SPACE_STAMP_ONCE") == nullptr; + auto& spaceUse = state.spaceUses[current->serial]; + if (!stampOnce || spaceUse == 0) { + for (const auto& region : current->base) { + if (region.direct == nullptr) continue; + region.direct->lastUse = stamp; + if (stampOnce) region.direct->spaceSerial = current->serial; + } + } + spaceUse = stamp; + if (state.spaceUses.size() > 64) { + const auto oldest = std::min_element(state.spaceUses.begin(), state.spaceUses.end(), [](const auto& left, const auto& right) { return left.second < right.second; }); + for (auto& [base, entry] : state.imports) { + if (entry.spaceSerial != oldest->first) continue; + entry.lastUse = std::max(entry.lastUse, oldest->second); + entry.spaceSerial = 0; + } + state.spaceUses.erase(oldest); } } } @@ -3417,15 +3730,25 @@ void GuestBufferMemory::AcquireRegistered() { regions.reserve(lease.size()); std::vector copies; std::vector imported; + const HostImport* covering = nullptr; + std::uint64_t coveredBegin = 0; + std::uint64_t coveredEnd = 0; for (const auto& range : lease) { if (!range->readable || !range->gpu) continue; validate(range->address, range->bytes); Region region{range->address, range->address + range->bytes, range->writable, {}, nullptr}; const HostImport* entry = nullptr; - if (importable) { + if (importable && covering != nullptr && range->address >= coveredBegin && range->address + range->bytes <= coveredEnd) { + entry = covering; + } else if (importable) { auto& state = Imports(); std::lock_guard lock(state.mutex); - entry = importAllocation(context, state, range->address, lease, 0, false); + entry = importAllocation(context, state, range->address, lease, ImportRequester::Build, 0, false); + covering = entry; + if (entry != nullptr) { + coveredBegin = entry->base; + coveredEnd = entry->base + entry->bytes; + } } lap(timing.importsUs, at); if (entry != nullptr) { @@ -3807,10 +4130,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; @@ -4231,6 +4553,7 @@ void GuestBufferMemory::UploadFinish(bool addressable) { for (const auto& region : regions) CompletePendingCpuWrites(context, region.begin, static_cast(region.end - region.begin)); if (space != nullptr) completeSpaceWrites(); uploaded = true; + for (const auto& region : regions) static_cast(FlushDepthPlanes(region.begin, static_cast(region.end - region.begin))); for (const auto& [begin, end] : writes) StorageTexture::FlushPending(begin, static_cast(end - begin), nullptr, "written buffer range"); if (context.hostImportAlignment != 0) { auto& state = Imports(); @@ -4335,7 +4658,7 @@ void GuestBufferMemory::UploadFinish(bool addressable) { if (entry == nullptr) entry = findImport(state, region.begin, region.end); const bool sparseRun = region.sparseHeap && region.sparse; if (entry == nullptr && !sparseRun) { - entry = importAccess(context, state, region.begin, region.end, importRanges()); + entry = importAccess(context, state, region.begin, region.end, importRanges(), ImportRequester::Build); } bool runImport = false; if (entry == nullptr && sparseRun && !region.backed.empty() && region.backed.front().first == region.begin && !WritesOverlap(region.begin, static_cast(bytes))) { @@ -4343,7 +4666,7 @@ void GuestBufferMemory::UploadFinish(bool addressable) { const auto neededEnd = std::min(region.end, std::max(committedEnd, std::max(region.lastView, region.begin) + 4u)); const auto covers = [&](const HostImport* candidate) { return candidate != nullptr && candidate->base <= region.begin && candidate->base + candidate->bytes >= neededEnd; }; entry = findImport(state, region.begin, neededEnd); - if (entry == nullptr && runCovers(importRanges(), region.begin, neededEnd)) entry = importAllocation(context, state, region.begin, importRanges()); + if (entry == nullptr && runCovers(importRanges(), region.begin, neededEnd)) entry = importAllocation(context, state, region.begin, importRanges(), ImportRequester::Build); if (!covers(entry)) entry = nullptr; const auto& lease = importRanges(); const auto index = leasedIndexContaining(lease, region.begin); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp index 413eeced6a..e08b9e47fd 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp @@ -99,9 +99,7 @@ GuestSamplerResource DecodeSamplerResource(std::span words, Require(!unnormalizedProven, "guest sampler descriptor is bound as unnormalized without FORCE_UNNORMALIZED"); } Require(!forceSrgb || forceDegammaPaired, "guest sampler descriptor forces sRGB decoding which is not implemented"); - // TRUNC_COORD picks point-sampled texels by truncation instead of rounding, and the perf fields - // trade mip/depth precision for speed; Vulkan's nearest filtering already floors, so these only - // move texel selection by half a texel at most and are accepted as is. ANISO_THRESHOLD and + // The perf fields trade mip/depth precision for speed and are accepted as is. ANISO_THRESHOLD and // ANISO_BIAS only tune how many anisotropic taps the hardware takes; the host filter picks its own. static_cast(truncCoord); static_cast(anisoThreshold); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp index 3af05d4ce7..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; @@ -251,6 +251,15 @@ bool DescriptorSingleLevel(std::span words) { return ((words[3] >> 12u) & 0xfu) == ((words[3] >> 16u) & 0xfu); } +bool FirstLayerDescriptor(std::span words, std::array& narrowed) { + if (words.size() != narrowed.size()) return false; + const auto resource = DecodeTextureResource(words); + if (resource.dimension != TextureDimension::k2DArray || resource.depthOrLastArray <= resource.baseArray) return false; + std::copy(words.begin(), words.end(), narrowed.begin()); + narrowed[4] = (narrowed[4] & ~0x1fffu) | resource.baseArray; + return true; +} + bool MatchesGuestDimension(ShaderRecompiler::DescriptorImageShape shape, TextureDimension dimension) { switch (shape) { case ShaderRecompiler::DescriptorImageShape::Image1D: return dimension == TextureDimension::k1D; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleReadback.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleReadback.cpp index 8f731eb881..fe92fa1b97 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleReadback.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleReadback.cpp @@ -46,10 +46,8 @@ VkPipeline TextureDetiler::multisampleReadbackPipeline(std::uint32_t elementByte } auto& module = readbackModules[depth ? 1u : 0u]; if (module == VK_NULL_HANDLE) { - VkShaderModuleCreateInfo info{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - info.codeSize = depth ? sizeof(MULTISAMPLE_READBACK_DEPTH_SPV) : sizeof(MULTISAMPLE_READBACK_UINT_SPV); - info.pCode = depth ? MULTISAMPLE_READBACK_DEPTH_SPV : MULTISAMPLE_READBACK_UINT_SPV; - Check(context.Function("vkCreateShaderModule")(context.device, &info, nullptr, &module), "vkCreateShaderModule multisample readback"); + module = depth ? CreateShaderModule(context, MULTISAMPLE_READBACK_DEPTH_SPV, sizeof(MULTISAMPLE_READBACK_DEPTH_SPV), "vkCreateShaderModule multisample readback") + : CreateShaderModule(context, MULTISAMPLE_READBACK_UINT_SPV, sizeof(MULTISAMPLE_READBACK_UINT_SPV), "vkCreateShaderModule multisample readback"); } const VkSpecializationMapEntry entry{0u, 0u, sizeof(elementBytes)}; const VkSpecializationInfo specialization{1u, &entry, sizeof(elementBytes), &elementBytes}; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 3edcb4287d..8911499a06 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -6,6 +6,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/SampleOrder.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" @@ -21,6 +22,7 @@ #include #include #include +#include #include #include @@ -35,10 +37,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{}; @@ -72,10 +86,13 @@ public: context.formatProperties(context.physical, target.format, &properties); Require((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) != 0, "multisampled render-target format cannot be a color attachment"); usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + orderFormat = MayReorderSamples(context, target.samples) ? SampleOrderStorageFormat(context, target.format, target.elementBytes, target.samples) : VK_FORMAT_UNDEFINED; + const VkImageCreateFlags orderFlags = orderFormat != VK_FORMAT_UNDEFINED ? VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | VK_IMAGE_CREATE_EXTENDED_USAGE_BIT : 0u; + if (orderFormat != VK_FORMAT_UNDEFINED) usage |= VK_IMAGE_USAGE_STORAGE_BIT; VkImageFormatProperties supported{}; - sampleable = (properties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) != 0 && context.imageFormatProperties(context.physical, target.format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage | VK_IMAGE_USAGE_SAMPLED_BIT, 0, &supported) == VK_SUCCESS && (supported.sampleCounts & samples) != 0; + sampleable = (properties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) != 0 && context.imageFormatProperties(context.physical, target.format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage | VK_IMAGE_USAGE_SAMPLED_BIT, orderFlags, &supported) == VK_SUCCESS && (supported.sampleCounts & samples) != 0; if (sampleable) usage |= VK_IMAGE_USAGE_SAMPLED_BIT; - Check(context.imageFormatProperties(context.physical, target.format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage, 0, &supported), "vkGetPhysicalDeviceImageFormatProperties multisampled target"); + Check(context.imageFormatProperties(context.physical, target.format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage, orderFlags, &supported), "vkGetPhysicalDeviceImageFormatProperties multisampled target"); Require((supported.sampleCounts & samples) != 0 && target.extent.width <= supported.maxExtent.width && target.extent.height <= supported.maxExtent.height, "multisampled render target exceeds device image limits"); storageFormat = HostStorageFormat(context, target.format); storageFlags = storageFormat != target.format ? VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | VK_IMAGE_CREATE_EXTENDED_USAGE_BIT : 0u; @@ -85,7 +102,7 @@ public: }; if (storageFormat != VK_FORMAT_UNDEFINED && (!context.storageImageMultisample || !storageSupports(storageFormat, VK_IMAGE_USAGE_STORAGE_BIT, 0u) || !storageSupports(target.format, usage | VK_IMAGE_USAGE_STORAGE_BIT, storageFlags))) storageFormat = VK_FORMAT_UNDEFINED; try { - const auto created = allocate(usage, 0u); + const auto created = allocate(usage, orderFlags); image = created.image; memory = created.memory; view = created.view; @@ -142,6 +159,9 @@ public: viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; viewInfo.format = target.format; viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + VkImageViewUsageCreateInfo viewUsage{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO}; + viewUsage.usage = imageUsage & ~VK_IMAGE_USAGE_STORAGE_BIT; + if (orderFormat != VK_FORMAT_UNDEFINED) viewInfo.pNext = &viewUsage; Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &created.view), "vkCreateImageView multisampled target"); } catch (...) { destroy(created); @@ -200,9 +220,10 @@ public: } void ApplyFastClear(const ColorTarget& use) { - checkDccCurrent(use); VkClearColorValue clear{}; - if (!TakeMultisampledClear(cmask, colorFill, use, clear)) return; + const bool refilled = absorbDccFill(use, clear); + checkDccCurrent(use, refilled); + if (!refilled && !TakeMultisampledClear(cmask, colorFill, use, clear)) return; Keep(); recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) { RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); @@ -213,6 +234,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; @@ -253,6 +292,30 @@ public: return (usage & VK_IMAGE_USAGE_STORAGE_BIT) != 0; } + VkImageView OrderView() { + Require(orderFormat != VK_FORMAT_UNDEFINED, "the multisampled color target has no storage view to put its samples in the guest's order"); + if (orderView != VK_NULL_HANDLE) return orderView; + VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + viewInfo.image = image; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = orderFormat; + viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + VkImageViewUsageCreateInfo viewUsage{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO}; + viewUsage.usage = VK_IMAGE_USAGE_STORAGE_BIT; + viewInfo.pNext = &viewUsage; + Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &orderView), "vkCreateImageView multisampled target storage"); + return orderView; + } + + void NoteMetadataFill(std::uint64_t fillAddress, std::size_t fillBytes, std::uint32_t pattern) { + const auto note = [&](std::uint64_t address, std::uint64_t bytes, std::optional& filled) { + if (address == 0 || fillAddress >= address + bytes || address >= fillAddress + fillBytes) return; + filled = fillAddress <= address && fillAddress + fillBytes >= address + bytes ? std::optional(pattern) : std::nullopt; + }; + note(target.dccAddress, MultisampledDccBytes(target), dccFill); + note(target.cmaskAddress, target.cmaskBytes, cmaskFill); + } + std::shared_ptr Sampled(VkComponentMapping components, VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D) { Require(sampleable, "the device cannot sample the multisampled color target's format at its sample count"); Require(cmask.Refusal().empty(), cmask.Refusal()); @@ -269,7 +332,7 @@ public: }); const std::array key{static_cast(components.r), static_cast(components.g), static_cast(components.b), static_cast(components.a), static_cast(viewType)}; auto& texture = textures[key]; - if (texture == nullptr) texture = std::make_shared(context, image, target.format, VK_IMAGE_ASPECT_COLOR_BIT, components, viewType); + if (texture == nullptr) texture = std::make_shared(context, image, target.format, VK_IMAGE_ASPECT_COLOR_BIT, components, viewType, 0, std::shared_ptr{}, orderFormat != VK_FORMAT_UNDEFINED ? VK_IMAGE_USAGE_SAMPLED_BIT : VkImageUsageFlags{0}); return {shared_from_this(), texture.get()}; } @@ -317,6 +380,8 @@ public: MultisampledCmask cmask; MultisampledColorFill colorFill; bool fromTexture = false; + VkFormat orderFormat = VK_FORMAT_UNDEFINED; + VkImageView orderView = VK_NULL_HANDLE; std::uint64_t htileAddress = 0; std::uint64_t htileBytes = 0; std::string htileRefusal; @@ -333,6 +398,34 @@ private: std::map, std::shared_ptr> textures; std::vector> retiredTextures; std::vector dccRanges; + std::optional dccFill; + std::optional cmaskFill; + + bool absorbDccFill(const ColorTarget& use, VkClearColorValue& clear) { + const auto dcc = std::exchange(dccFill, std::nullopt); + const auto cmaskPattern = std::exchange(cmaskFill, std::nullopt); + if (dccRanges.size() < 2 || !dcc || use.dccAddress != target.dccAddress || use.cmaskAddress != target.cmaskAddress || use.cmaskBytes != target.cmaskBytes) return false; + const auto keys = LandedUniformMetadataByte(dccRanges[1].address, dccRanges[1].bytes); + if (!keys || *dcc != *keys * 0x01010101u) return false; + std::uint8_t cmaskKeys = 0; + if (dccRanges.size() > 2) { + const auto& range = dccRanges[2]; + auto* recorder = Recorder::Active(); + const bool written = !GuestMemory::UnchangedSince(range.address, range.bytes, range.generation) || (recorder != nullptr && recorder->PendingWriteOverlaps(range.address, range.bytes)); + const auto landed = LandedUniformMetadataByte(range.address, range.bytes); + if (!landed || (written && (!cmaskPattern || *cmaskPattern != *landed * 0x01010101u))) return false; + cmaskKeys = *landed; + } + std::vector metadata(dccRanges.begin() + 1, dccRanges.end()); + for (auto& range : metadata) { + if (!GuestMemory::Watched(range.address, range.bytes)) return false; + range.generation = collectBlocks(range.address, range.bytes); + if (range.generation == 0) return false; + } + clear = MultisampledDccClear(use, std::vector(dccRanges[1].bytes, std::byte{*keys}), std::vector(use.cmaskBytes, std::byte{cmaskKeys})); + std::copy(metadata.begin(), metadata.end(), dccRanges.begin() + 1); + return true; + } VkClearColorValue initialDccClear(const ColorTarget& use) { if (use.dccAddress == 0) return {}; @@ -347,7 +440,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) { @@ -374,7 +467,7 @@ private: Require(false, text); } - void checkDccCurrent(const ColorTarget& use) const { + void checkDccCurrent(const ColorTarget& use, bool metadataRefilled) const { if (dccRanges.empty()) { Require(use.dccAddress == 0, "adding DCC to a resident multisampled color target is not implemented"); return; @@ -382,8 +475,9 @@ private: Require(metadataValid->load(std::memory_order_acquire), "multisampled DCC metadata was released while its color target remains resident"); static_cast(MultisampledDccBytes(use)); Require(use.address == target.address && use.dccAddress == target.dccAddress && use.dccControl == target.dccControl && use.dccAlphaOnMsb == target.dccAlphaOnMsb && use.dccPipeAligned == target.dccPipeAligned && use.cmaskAddress == target.cmaskAddress && use.cmaskBytes == target.cmaskBytes && use.tileMode == target.tileMode && use.elementBytes == target.elementBytes && use.pipeBankXor == target.pipeBankXor, "multisampled DCC target layout or metadata changed"); - for (const auto& range : dccRanges) { - requireDccRange(GuestMemory::Watched(range.address, range.bytes), range, "multisampled DCC target reuse requires watched guest memory"); + const auto ranges = std::span(dccRanges).first(metadataRefilled ? 1u : dccRanges.size()); + for (const auto& range : ranges) requireDccRange(GuestMemory::Watched(range.address, range.bytes), range, "multisampled DCC target reuse requires watched guest memory"); + for (const auto& range : ranges) { GuestMemory::CollectWritesUncached(range.address, range.bytes); requireDccRange(GuestMemory::UnchangedSince(range.address, range.bytes, range.generation) || !GuestMemory::StoredOver(range.address, range.bytes, range.generation), range, "writes to a resident multisampled DCC target or its metadata are not implemented"); if (auto* recorder = Recorder::Active(); recorder != nullptr) requireDccRange(!recorder->PendingWriteOverlaps(range.address, range.bytes), range, "pending writes to a resident multisampled DCC target or its metadata are not implemented"); @@ -394,6 +488,7 @@ private: textures.clear(); retiredTextures.clear(); if (resolvePass) context.Function("vkDestroyRenderPass")(context.device, resolvePass, nullptr); + if (orderView) context.Function("vkDestroyImageView")(context.device, orderView, nullptr); if (storageView) context.Function("vkDestroyImageView")(context.device, storageView, nullptr); destroy({image, memory, view}); for (const auto& allocation : retired) destroy(allocation); @@ -436,7 +531,38 @@ void guardReads(std::uint64_t address, std::size_t bytes) { RequireNoMultisampleTarget(address, bytes, "a CPU read"); } +struct RecordedSingleClear { + std::uint64_t surface = 0; + std::uint32_t pipeBankXor = 0; + std::uint32_t width = 0; + std::uint32_t height = 0; + std::uint32_t samples = 0; + std::array color{}; +}; + +std::mutex& singleClearsMutex() { + static std::mutex mutex; + return mutex; +} + +std::vector& singleClears() { + static std::vector list; + return list; +} + +std::atomic& anySingleClears() { + static std::atomic any{false}; + return any; +} + +void forgetSingleClears(std::uintptr_t address, std::size_t bytes) { + std::lock_guard lock(singleClearsMutex()); + std::erase_if(singleClears(), [&](const RecordedSingleClear& clear) { return clear.surface >= address && clear.surface - address < bytes; }); + anySingleClears().store(!singleClears().empty(), std::memory_order_release); +} + void retireUnmapped(std::uintptr_t address, std::size_t bytes) { + forgetSingleClears(address, bytes); RetireUnmappedDepth_nid_no_patch(address, bytes); std::unique_lock lock(guardedMutex()); std::erase_if(guarded(), [&](const auto& range) { @@ -489,6 +615,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); @@ -596,7 +733,7 @@ void InstallResidentImageReadGuard_nid_no_patch() { void UpdateResidentImageUnmapObserver_nid_no_patch() { static std::mutex mutex; std::lock_guard lock(mutex); - GuestAllocations::GuestAllocationsSetUnmapObserver_nid_postfix(anyGuarded().load(std::memory_order_acquire) || HasResidentDepthGuards_nid_no_patch() ? &retireUnmapped : nullptr); + GuestAllocations::GuestAllocationsSetUnmapObserver_nid_postfix(anyGuarded().load(std::memory_order_acquire) || anySingleClears().load(std::memory_order_acquire) || HasResidentDepthGuards_nid_no_patch() ? &retireUnmapped : nullptr); } std::optional MultisampledCmaskKeys(std::uint8_t code) { @@ -674,6 +811,61 @@ std::size_t MultisampledDccBytes(const ColorTarget& target) { return static_cast(((target.extent.width + width - 1) / width) * ((target.extent.height + height - 1) / height) * 4096u); } +namespace { + +std::optional> recordedSingleClear(const ColorTarget& target) { + std::lock_guard lock(singleClearsMutex()); + for (const auto& clear : singleClears()) { + if (clear.surface == target.surfaceAddress && clear.pipeBankXor == target.pipeBankXor && clear.width == target.extent.width && clear.height == target.extent.height && clear.samples == target.samples) return clear.color; + } + return std::nullopt; +} + +VkClearColorValue singleClearColor(const ColorTarget& target) { + const auto color = recordedSingleClear(target); + Require(color.has_value(), "multisampled DCC comp-to-single keys without a recorded clear color are not implemented"); + const auto elementBytes = static_cast(target.elementBytes); + Require(elementBytes == 1 || elementBytes == 2 || elementBytes == 4 || elementBytes == 8 || elementBytes == 16, "multisampled DCC comp-to-single clears of this texel size are not implemented"); + std::array pattern{}; + for (std::size_t offset = 0; offset < sizeof(pattern); offset += elementBytes) std::memcpy(reinterpret_cast(pattern.data()) + offset, color->data(), elementBytes); + VkClearColorValue clear{}; + Require(ClearColorForTexel(target.format, target.elementBytes, pattern, clear), "multisampled DCC comp-to-single clear has no exact host value"); + return clear; +} + +} + +std::optional MatchMultisampledSingleClear(std::span code, std::span userData, const std::array& numThreads) { + static constexpr std::array kernel{0xbe800303u, 0xbe830382u, 0xf40c0401u, 0xfa000000u, 0xd7460000u, 0x04010609u, 0xd7460001u, 0x0405060au, 0x7e060208u, 0x7e0c020bu, 0x7e0e0280u, 0xd5690004u, 0x02000100u, 0x7e000205u, 0xd5690005u, 0x02000901u, 0xd7000001u, 0x02000c80u, 0xbf8cc07fu, 0xf0201f38u, 0x00040004u, 0xbf810000u}; + if (userData.size() < 9 || numThreads != std::array{8u, 8u, 1u}) return std::nullopt; + if (code.size() < kernel.size() || !std::equal(kernel.begin(), kernel.end(), code.begin())) return std::nullopt; + if (userData[3] == 0 || userData[4] == 0) return std::nullopt; + return MultisampledSingleClear{userData[2] | (std::uint64_t{2} << 32u), {userData[5], userData[6], userData[7], userData[8]}}; +} + +void NoteMultisampledSingleClear(const GuestTextureResource& resource, const std::array& color) { + { + std::lock_guard lock(singleClearsMutex()); + auto& list = singleClears(); + std::erase_if(list, [&](const RecordedSingleClear& clear) { return clear.surface == resource.baseAddress; }); + list.push_back({resource.baseAddress, resource.pipeBankXor, resource.width, resource.height, resource.samples, color}); + anySingleClears().store(true, std::memory_order_release); + } + UpdateResidentImageUnmapObserver_nid_no_patch(); +} + +bool TakeMultisampledSingleClear(std::span code, std::span userData, const std::array& numThreads) { + const auto clear = MatchMultisampledSingleClear(code, userData, numThreads); + if (!clear) return false; + std::array words{}; + if (!GuestMemory::Accessible(reinterpret_cast(clear->descriptor), sizeof(words))) return false; + GuestMemory::Read(clear->descriptor, std::as_writable_bytes(std::span(words))); + const auto resource = DecodeTextureResource(words); + if (resource.samples < 2) return false; + NoteMultisampledSingleClear(resource, clear->color); + return true; +} + VkClearColorValue MultisampledDccClear(const ColorTarget& target, std::span dcc, std::span cmask) { Require(target.dccAddress != 0 && (target.dccControl & 0x3ff80u) == 0 && (target.pipeBankXor & ~0xcf00u) == 0, "multisampled DCC address, color transform, lossy mode or XOR is not implemented"); Require(dcc.size() == MultisampledDccBytes(target), "multisampled DCC metadata snapshot size mismatch"); @@ -684,6 +876,7 @@ VkClearColorValue MultisampledDccClear(const ColorTarget& target, std::span(dcc.front())) { case 0x00u: keys = DccKeys::Clear0000; break; @@ -755,6 +948,12 @@ bool TakeMultisampledClear(MultisampledCmask& cmask, MultisampledColorFill& colo return true; } +SampleOrderTarget MultisampleTargetOrderTarget(const Context& context, const ColorTarget& target) { + std::lock_guard lock(targetsMutex()); + auto& found = acquire(context, target); + return {found.image, found.OrderView(), found.orderFormat, target.extent, target.samples, SampleOrderMethod::Storage, found.shared_from_this()}; +} + VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target, VkImage* image) { std::lock_guard lock(targetsMutex()); const auto& acquired = acquire(context, target); @@ -896,6 +1095,10 @@ void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32 target->htileRefusal = text; } } + if (target->target.dccAddress != 0) { + target->NoteMetadataFill(address, bytes, pattern); + continue; + } const auto before = target->cmask.Keys(); target->cmask.NoteFill(address, bytes, pattern); if (before == DccKeys::Clear0000 || target->cmask.Keys() != DccKeys::Clear0000) continue; @@ -915,6 +1118,10 @@ void ClearMultisampleTargets(VkDevice device) { void RequireNoMultisampleTarget(std::uint64_t address, std::size_t bytes, const char* access) { RequireNoResidentDepthRead_nid_no_patch(address, bytes); + RequireNoMultisampleColorTarget(address, bytes, access); +} + +void RequireNoMultisampleColorTarget(std::uint64_t address, std::size_t bytes, const char* access) { if (bytes == 0 || !anyGuarded().load(std::memory_order_acquire)) return; std::shared_lock lock(guardedMutex()); for (const auto& range : guarded()) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PassHazards.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PassHazards.cpp index a3a4c97382..58673799a3 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PassHazards.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PassHazards.cpp @@ -34,15 +34,16 @@ bool PassHazards::overlapsAny(const GuestRangeSet& set, GuestRangeList ranges) c PassHazard PassHazards::Check(const PassAccess& access, bool samePass) const { const auto touchesMemory = [](const PassAccess& use) { const auto nonEmpty = [](GuestRangeList ranges) { return std::any_of(ranges.begin(), ranges.end(), [](const auto& range) { return range.first < range.second; }); }; - return use.anyReads || use.anyWrites || nonEmpty(use.reads) || nonEmpty(use.writes); + return use.anyReads || use.anyWrites || nonEmpty(use.reads) || nonEmpty(use.writes) || nonEmpty(use.writeOnly); }; - const bool earlierTouches = anyReads || anyWrites || !reads.Empty() || !writes.Empty(); + const bool earlierTouches = anyReads || anyWrites || !reads.Empty() || !writes.Empty() || !writeOnly.Empty(); if ((access.anyWrites && earlierTouches) || (anyWrites && touchesMemory(access))) return PassHazard::UnknownWrite; - const bool writesRanges = std::any_of(access.writes.begin(), access.writes.end(), [](const auto& range) { return range.first < range.second; }); - if ((access.anyReads && !writes.Empty()) || (anyReads && writesRanges)) return PassHazard::UnknownRead; - if (overlapsAny(writes, access.reads)) return PassHazard::ReadAfterWrite; - if (overlapsAny(writes, access.writes)) return PassHazard::WriteAfterWrite; - if (overlapsAny(reads, access.writes)) return PassHazard::WriteAfterRead; + const auto nonEmpty = [](GuestRangeList ranges) { return std::any_of(ranges.begin(), ranges.end(), [](const auto& range) { return range.first < range.second; }); }; + const bool writesRanges = nonEmpty(access.writes) || nonEmpty(access.writeOnly); + if ((access.anyReads && (!writes.Empty() || !writeOnly.Empty())) || (anyReads && writesRanges)) return PassHazard::UnknownRead; + if (overlapsAny(writes, access.reads) || overlapsAny(writeOnly, access.reads)) return PassHazard::ReadAfterWrite; + if (overlapsAny(writes, access.writes) || overlapsAny(writeOnly, access.writes) || overlapsAny(writes, access.writeOnly)) return PassHazard::WriteAfterWrite; + if (overlapsAny(reads, access.writes) || overlapsAny(reads, access.writeOnly)) return PassHazard::WriteAfterRead; const auto conflicts = [](bool earlier, bool later) { return earlier || later; }; for (const auto& [image, written] : access.images) { if (image == VK_NULL_HANDLE) continue; @@ -60,6 +61,7 @@ PassHazard PassHazards::Check(const PassAccess& access, bool samePass) const { void PassHazards::Add(const PassAccess& access) { for (const auto& [begin, end] : access.reads) reads.Insert(begin, end); for (const auto& [begin, end] : access.writes) writes.Insert(begin, end); + for (const auto& [begin, end] : access.writeOnly) writeOnly.Insert(begin, end); for (const auto& [image, written] : access.images) { if (image == VK_NULL_HANDLE) continue; const auto it = std::find_if(images.begin(), images.end(), [&](const auto& seen) { return seen.first == image; }); @@ -76,16 +78,22 @@ void PassHazards::Add(const PassAccess& access) { anyWrites = anyWrites || access.anyWrites; } +void PassHazards::SealWriteOnly() { + for (const auto& [begin, end] : writeOnly.Ranges()) writes.Insert(begin, end); + writeOnly.Clear(); +} + void PassHazards::Clear() { reads.Clear(); writes.Clear(); + writeOnly.Clear(); images.clear(); attachments.clear(); anyReads = anyWrites = false; } bool PassHazards::Empty() const { - return reads.Empty() && writes.Empty() && images.empty() && attachments.empty() && !anyReads && !anyWrites; + return reads.Empty() && writes.Empty() && writeOnly.Empty() && images.empty() && attachments.empty() && !anyReads && !anyWrites; } } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index 465fa91e9e..20ce2e4e8b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -6,6 +6,7 @@ #include "prx/libSceAgcDriver/Graphics/include/PipelineJournal.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/PipelineSpecialization.hpp" +#include "prx/libSceAgcDriver/Graphics/include/SampleOrder.hpp" #include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp" #include "prx/libc/include/HostThreadLocal.hpp" #include @@ -111,9 +112,16 @@ void ValidateProvokingVertex(const Context& context, const State& state, std::sp } return; } + if (BarycentricPatches(state, shaders)) { + for (const auto& shader : shaders) { + if (shader.stage != ShaderRecompiler::ShaderStage::Fragment || shader.program == nullptr) continue; + Require(std::none_of(shader.program->fragmentParameters.begin(), shader.program->fragmentParameters.end(), [](const ShaderRecompiler::FragmentParameter& parameter) { return parameter.flat && parameter.perVertex; }), "last provoking vertex with explicit per-vertex flat interpolation is unsupported"); + } + return; + } Require(context.provokingVertexLast, "last provoking vertex requires VK_EXT_provoking_vertex with provokingVertexLast enabled"); const bool meshPrimitives = state.stages.path == ShaderPath::Geometry && state.stages.mesh; - Require(!BarycentricPatches(state, shaders) && !state.stages.tessellation && (meshPrimitives || (state.stages.path == ShaderPath::Vertex && !state.stages.mesh)), "last provoking vertex is unsupported for generated primitive pipelines"); + Require(!state.stages.tessellation && (meshPrimitives || (state.stages.path == ShaderPath::Vertex && !state.stages.mesh)), "last provoking vertex is unsupported for generated primitive pipelines"); Require(!meshPrimitives || (state.topology != VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP && state.topology != VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN), "last provoking vertex is unsupported for triangle strips or fans assembled for a geometry program"); Require(state.rectList || state.topology == VK_PRIMITIVE_TOPOLOGY_POINT_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_STRIP || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN, "last provoking vertex is unsupported for this primitive topology"); for (const auto& shader : shaders) { @@ -169,10 +177,11 @@ bool ShadersWriteMemory(std::span shaders) { void ValidateSampleMask(const Context& context, const State& state, std::span shaders) { if (!PartialSampleMask(state)) return; + const bool uniform = UniformSampleMask(state) && !SampleMaskFollowsOrder(state, HostSampleLocationsFor(context, state).order); for (const auto& shader : shaders) { if (shader.stage != ShaderRecompiler::ShaderStage::Fragment) continue; Require(shader.program != nullptr, "missing compiled fragment shader"); - if (UniformSampleMask(state)) { + if (uniform) { Require(context.earlyFragmentSampleMaskTestBeforeSampleCounting || !DeclaresEarlyFragmentTests(*shader.program), "a sample mask with early fragment tests needs VkPhysicalDeviceMaintenance5Properties::earlyFragmentSampleMaskTestBeforeSampleCounting, which the device lacks"); continue; } @@ -190,13 +199,6 @@ VkShaderStageFlags pushStagesOf(const Context& context) { return PreRasterizationPushStages(context.meshShader, context.tessellationShader, context.geometryShader); } -VkExtent2D hostSampleLocationGrid(const Context& context, const State& state) { - if (!state.sampleLocationGrid || (context.sampleLocationGridSampleCounts & state.samples) != 0) return SampleLocationGrid(state); - static std::atomic reported{0}; - if ((reported.fetch_or(state.samples, std::memory_order_relaxed) & state.samples) == 0) std::fprintf(stderr, "[gpu] no 2x2 sample location grid at sample count %u on this device: sample locations varying between pixels take pixel (0,0)'s in every pixel\n", state.samples); - return {1, 1}; -} - template void appendKey(std::vector& key, const TValue& value) { static_assert(std::is_trivially_copyable_v); @@ -224,6 +226,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); @@ -239,6 +246,7 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& appendKey(keys.vertexInput, state.topology); appendKey(keys.vertexInput, state.primitiveRestart); appendKey(keys.preRasterization, state.provokingVertexMode); + appendKey(keys.preRasterization, state.polygonMode); appendKey(keys.preRasterization, state.negativeOneToOne); appendKey(keys.preRasterization, state.depthClamp && context.depthClamp); appendKey(keys.preRasterization, state.conservativeRasterization); @@ -278,6 +286,12 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& if (state.depth) appendKey(keys.renderPass, state.depth->format); appendKey(keys.fragmentShader, HostSampleMask(state)); appendKey(keys.fragmentOutput, HostSampleMask(state)); + for (auto* key : {&keys.fragmentShader, &keys.fragmentOutput}) { + appendKey(*key, context.sampleLocationSampleCounts); + appendKey(*key, context.sampleLocationGridSampleCounts); + appendKey(*key, context.storageImageMultisample); + appendKey(*key, context.shaderStencilExport); + } if (!resources.split) { appendKey(keys.layout, pushStagesOf(context)); for (const auto word : resources.mainKey) appendKey(keys.layout, word); @@ -310,7 +324,9 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput Pipeline::Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span shaders, VkImageLayout attachmentLayout) : context(context), _modules(shaders.size()), attachments(state.colors.size() + (state.depth ? 1u : 0u)), colorAttachments(state.colors.size()), depthBounds(state.depth.has_value() && state.depthBoundsTest), depthBias(state.depth.has_value() && state.depthBias), dynamicRendering(context.graphicsPipelineLibrary) { PerformanceTimer timing("Vulkan.GraphicsPipeline"); sampleLocationsEnabled = ValidateSampleLocations(context, state); - sampleLocationGrid = hostSampleLocationGrid(context, state); + const auto host = HostSampleLocationsFor(context, state); + sampleLocationGrid = host.grid; + sampleOrder = host.order; // A cached pipeline may outlive its device's teardown (see ClearCachedPipelines); it must not keep // the buffer pool, which is reset with the device, alive past it. this->context.bufferPool.reset(); @@ -354,10 +370,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput const VkPipelineShaderStageRequiredSubgroupSizeCreateInfoEXT meshSubgroup{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_REQUIRED_SUBGROUP_SIZE_CREATE_INFO_EXT, nullptr, 32u}; for (std::uint32_t i = 0; i < shaders.size(); ++i) { const auto& shader = *shaders[i].program; - VkShaderModuleCreateInfo module{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - module.codeSize = shader.spirv.size() * sizeof(std::uint32_t); - module.pCode = shader.spirv.data(); - Check(context.Function("vkCreateShaderModule")(context.device, &module, nullptr, &_modules[i]), "vkCreateShaderModule graphics"); + _modules[i] = CreateShaderModule(context, shader.spirv.data(), shader.spirv.size() * sizeof(std::uint32_t), "vkCreateShaderModule graphics"); const auto stage = VulkanStage(shaders[i].stage); stages[i].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[i].stage = stage; @@ -472,7 +485,9 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput dynamic.dynamicStateCount = static_cast(dynamicStates.size()); dynamic.pDynamicStates = dynamicStates.data(); VkPipelineRasterizationStateCreateInfo raster{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO}; - raster.polygonMode = VK_POLYGON_MODE_FILL; + Require(state.polygonMode == VK_POLYGON_MODE_FILL || context.fillModeNonSolid, "a point or line polygon mode needs the fillModeNonSolid feature"); + Require(state.polygonMode != VK_POLYGON_MODE_POINT || context.maintenance5, "a point polygon mode needs VK_KHR_maintenance5 for its one-pixel point size"); + raster.polygonMode = state.polygonMode; raster.depthClampEnable = state.depthClamp && context.depthClamp ? VK_TRUE : VK_FALSE; Require(!state.depthBiasPerFace || (depthBias && state.cullMode == VK_CULL_MODE_NONE), "per-face depth bias needs both faces rasterized"); raster.cullMode = state.depthBiasPerFace ? VK_CULL_MODE_BACK_BIT : state.cullMode; @@ -513,7 +528,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput depthStencil.front = state.stencilFront; depthStencil.back = state.stencilBack; const VkSampleMask sampleMask = HostSampleMask(state); - if (PartialSampleMask(state) && UniformSampleMask(state)) samples.pSampleMask = &sampleMask; + if (PartialSampleMask(state) && UniformSampleMask(state) && !SampleMaskFollowsOrder(state, sampleOrder)) samples.pSampleMask = &sampleMask; VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO}; const bool alternateAlpha = SplitsAlternateAlpha(state); std::vector firstChannels(state.blends.begin(), state.blends.end()); @@ -866,6 +881,7 @@ void pipelineKey(std::vector& key, const Context& context, const Stat append(key, state.topology); append(key, state.primitiveRestart); append(key, state.cullMode); + append(key, state.polygonMode); append(key, state.frontFace); append(key, state.provokingVertexMode); append(key, state.negativeOneToOne); @@ -935,6 +951,10 @@ void pipelineKey(std::vector& key, const Context& context, const Stat append(key, tessellation.breakAtInstance); } append(key, HostSampleMask(state)); + append(key, context.sampleLocationSampleCounts); + append(key, context.sampleLocationGridSampleCounts); + append(key, context.storageImageMultisample); + append(key, context.shaderStencilExport); } std::uint64_t hashKey(const std::vector& key) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp index a3cf852d6a..73fac4402d 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()) { @@ -80,7 +83,7 @@ PipelineCache::~PipelineCache() { } wake.notify_all(); saver.join(); - save(true); + save(true, true); } context.Function("vkDestroyPipelineCache")(context.device, cache, nullptr); } @@ -93,7 +96,12 @@ PipelineCache::File PipelineCache::readFile() const { File file; if (!ShaderRecompiler::ReadWholeFile(path, file.contents)) { std::error_code error; +#ifdef _WIN32 + const auto native = ShaderRecompiler::NativeCachePath_nid_no_patch(path, error); + const bool exists = !error && std::filesystem::exists(native, error); +#else const bool exists = std::filesystem::exists(path, error); +#endif file.state = exists || error ? FileState::Unreadable : FileState::Missing; return file; } @@ -138,7 +146,14 @@ PipelineCache::File PipelineCache::readFile() const { } std::optional PipelineCache::readHeader() const { +#ifdef _WIN32 + std::error_code error; + const auto native = ShaderRecompiler::NativeCachePath_nid_no_patch(path, error); + if (error) return std::nullopt; + std::ifstream file(native, std::ios::binary); +#else std::ifstream file(path, std::ios::binary); +#endif FileHeader header{}; if (!file.read(reinterpret_cast(&header), sizeof(header))) return std::nullopt; return header; @@ -164,12 +179,14 @@ void PipelineCache::load(std::vector& initialData) { std::fprintf(stderr, "[pipeline-cache] loaded %.1f KiB from %s\n", static_cast(initialData.size()) / 1024.0, path.string().c_str()); } -void PipelineCache::save(bool final) { +void PipelineCache::save(bool final, bool waitForPipelines) { std::lock_guard pathLock(pathMutex(path)); const auto getData = context.Function("vkGetPipelineCacheData"); + const auto snapshot = [&] { return waitForPipelines ? synchronization.LockForSnapshot() : synchronization.TryLockForSnapshot(); }; std::size_t liveBytes = 0; { - auto synchronizationLock = synchronization.LockForSnapshot(); + auto synchronizationLock = snapshot(); + if (!synchronizationLock.owns_lock()) return; if (getData(context.device, cache, &liveBytes, nullptr) != VK_SUCCESS || liveBytes == 0) return; } const auto onDisk = readHeader(); @@ -210,7 +227,8 @@ void PipelineCache::save(bool final) { } } if (merged != VK_NULL_HANDLE) { - auto synchronizationLock = synchronization.LockForSnapshot(); + auto synchronizationLock = snapshot(); + if (!synchronizationLock.owns_lock()) return; const auto result = context.Function("vkMergePipelineCaches")(context.device, merged, 1, &cache); if (result != VK_SUCCESS) { std::fprintf(stderr, "[pipeline-cache] cannot merge into %s (Vulkan result %d); not saving over it\n", path.string().c_str(), static_cast(result)); @@ -221,7 +239,8 @@ void PipelineCache::save(bool final) { std::size_t size = 0; std::vector file; { - auto synchronizationLock = source == cache ? synchronization.LockForSnapshot() : PipelineCacheSynchronization::SnapshotLock{}; + auto synchronizationLock = source == cache ? snapshot() : PipelineCacheSynchronization::SnapshotLock{}; + if (source == cache && !synchronizationLock.owns_lock()) return; if (getData(context.device, source, &size, nullptr) != VK_SUCCESS || size == 0) return; if (merged != VK_NULL_HANDLE && size == disk.header.dataBytes) { settle(disk.header); @@ -248,14 +267,18 @@ void PipelineCache::save(bool final) { } void PipelineCache::SaveNow() { - save(false); + save(false, true); +} + +void PipelineCache::SaveUnlessBusy() { + save(false, false); } void PipelineCache::run() { std::unique_lock lock(mutex); while (!wake.wait_for(lock, std::chrono::seconds(SaveIntervalSeconds), [&] { return stopping; })) { lock.unlock(); - save(false); + save(false, false); lock.lock(); } } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp index 45481c3c5f..d0669ed238 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp @@ -44,7 +44,7 @@ static_assert(sizeof(ShaderRecompiler::TessellationConfiguration) == 40, "Tessel constexpr std::uint32_t FileMagic = 0x4c4a5041u; // Bumped when the recipe codec, CanonicalPipelineState or the store key change in a way the // layout guards above do not catch (a field renumbered or reinterpreted at the same size). -constexpr std::uint32_t FileFormat = 4u; +constexpr std::uint32_t FileFormat = 6u; constexpr std::uint32_t ModuleRecord = 1u; constexpr std::uint32_t GraphicsRecord = 2u; constexpr std::uint32_t ComputeRecord = 3u; @@ -208,6 +208,7 @@ void encodeState(Writer& writer, const State& state) { writer.Bool(state.depthClamp); writer.U32(static_cast(state.conservativeRasterization)); writer.U32(state.cullMode); + writer.U32(static_cast(state.polygonMode)); writer.U32(static_cast(state.frontFace)); writer.U32(static_cast(state.provokingVertexMode)); writer.U32(state.samples); @@ -268,6 +269,7 @@ void decodeState(Reader& reader, State& state) { state.depthClamp = reader.Bool(); state.conservativeRasterization = static_cast(reader.U32()); state.cullMode = reader.U32(); + state.polygonMode = static_cast(reader.U32()); state.frontFace = static_cast(reader.U32()); state.provokingVertexMode = static_cast(reader.U32()); state.samples = reader.U32(); @@ -564,8 +566,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(); } @@ -1033,6 +1042,7 @@ State CanonicalPipelineState(const State& state) { canonical.depthClamp = state.depthClamp; canonical.conservativeRasterization = state.conservativeRasterization; canonical.cullMode = state.cullMode; + canonical.polygonMode = state.polygonMode; canonical.frontFace = state.frontFace; canonical.provokingVertexMode = state.provokingVertexMode; canonical.samples = state.samples; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index 0cf39a8ad7..9f03343a67 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -3,6 +3,7 @@ #include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" #include "prx/libc/include/HostThreadLocal.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libkernel/Time/include/SleepWake.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "prx/libSceAgcDriver/Graphics/include/ParallelCompare.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" @@ -32,6 +33,7 @@ #include #include #include +#include #include "prx/libc/include/general/AtomicSharedPtr.hpp" namespace AgcDriver::Graphics { @@ -1056,6 +1058,7 @@ void FlushForAccess(std::uint64_t address, std::size_t bytes) { std::size_t images = 0, evicted = 0; const bool kept = !HookFlushCpuBlocks() && StorageTexture::AccessKeptByCpu(address, bytes, &images, &evicted); if (kept || (images == 0 && !HookFlushCpuBlocks())) { + stored = StorageTexture::WaitForFlushing(address, bytes); published = StorageTexture::PublishShadowsOnly(address, bytes, scope); if (kept && HookSyncProfiled()) CountRecordedStoreSkip(RecordedStoreKey(), evicted); } else { @@ -1081,6 +1084,60 @@ void FlushForAccess(std::uint64_t address, std::size_t bytes) { } +struct Recorder::ProgressWatcher { + std::mutex mutex; + std::condition_variable wake; + std::uint64_t submitted = 0; + bool stop = false; + std::thread thread; + + ProgressWatcher(VkDevice device, VkSemaphore timeline, PFN_vkWaitSemaphoresKHR waitSemaphores, PFN_vkGetSemaphoreCounterValueKHR counterValue) { + thread = std::thread([this, device, timeline, waitSemaphores, counterValue] { + CpuTopology::PinHelperThread("GPU progress watcher"); + std::uint64_t seen = 0; + for (;;) { + { + std::unique_lock lock(mutex); + wake.wait(lock, [&] { return stop || submitted > seen; }); + if (stop) return; + } + const std::uint64_t target = seen + 1; + VkSemaphoreWaitInfoKHR info{VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO_KHR}; + info.semaphoreCount = 1; + info.pSemaphores = &timeline; + info.pValues = ⌖ + const auto result = waitSemaphores(device, &info, 50'000'000ull); + if (result == VK_TIMEOUT) continue; + if (result != VK_SUCCESS) { + std::fprintf(stderr, "[gpu] progress watcher: vkWaitSemaphoresKHR failed (%d), watcher stops\n", static_cast(result)); + return; + } + std::uint64_t value = target; + if (counterValue(device, timeline, &value) != VK_SUCCESS) value = target; + seen = std::max(target, value); + KernelWakeSleepers_nid_no_patch(); + } + }); + } + + void Submitted(std::uint64_t serial) { + { + std::lock_guard lock(mutex); + submitted = serial; + } + wake.notify_one(); + } + + ~ProgressWatcher() { + { + std::lock_guard lock(mutex); + stop = true; + } + wake.notify_one(); + thread.join(); + } +}; + Recorder::Recorder(const Context& context, bool timelineSemaphores) : context(context), id(nextRecorderId.fetch_add(1)) { { std::lock_guard lock(liveRecordersMutex); @@ -1127,6 +1184,7 @@ Recorder::~Recorder() { } catch (const std::exception& error) { std::fprintf(stderr, "[gpu] recorder teardown: %s\n", error.what()); } + progressWatcher.reset(); holdKept.clear(); if (activeRecorder == this) { activeRecorder = nullptr; @@ -1418,6 +1476,10 @@ void Recorder::LeaveRenderPassOpen(std::uint64_t key, std::uint32_t timing, bool open->passHazards.Add(access); } +void Recorder::SealPassWriteOnly() { + if (open != nullptr) open->passHazards.SealWriteOnly(); +} + void Recorder::endOpenRenderPass(bool barrier) { auto& pass = open->renderPass; if (open->segmentActive) endSegment(*open); @@ -1670,6 +1732,18 @@ bool Recorder::closeStoreRun(bool atSubmit) { std::vector writes; writes.reserve(stores.size()); for (const auto& store : stores) writes.push_back({store.address, store.End(), store.dumpTarget != 0}); + if (const auto order = QueuedWriteOrder(writes); !std::is_sorted(order.begin(), order.end())) { + std::vector ordered; + std::vector orderedWrites; + ordered.reserve(stores.size()); + orderedWrites.reserve(writes.size()); + for (const auto index : order) { + ordered.push_back(std::move(stores[index])); + orderedWrites.push_back(writes[index]); + } + stores = std::move(ordered); + writes = std::move(orderedWrites); + } const bool dumps = std::any_of(writes.begin(), writes.end(), [](const QueuedWrite& write) { return write.computed; }); if (dumps) recordQueuedDumps(commands, stores); std::size_t first = 0; @@ -1975,8 +2049,8 @@ void Recorder::EndGpuTiming(std::uint32_t index, std::uint64_t bytes) { context.Function("vkCmdWriteTimestamp")(open->commands, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, open->queries, index * 2 + 1); } -bool Recorder::RecordDispatchArguments(VkCommandBuffer commands, VkDeviceAddress source, VkDeviceAddress arguments) { - constexpr std::uint32_t PushBytes = 32; +bool Recorder::RecordDispatchArguments(VkCommandBuffer commands, VkDeviceAddress source, VkDeviceAddress arguments, const std::array* threadGroup) { + constexpr std::uint32_t PushBytes = 48; if (dispatchArgumentState == 0) { dispatchArgumentState = -1; VkShaderModule module = VK_NULL_HANDLE; @@ -1986,10 +2060,7 @@ bool Recorder::RecordDispatchArguments(VkCommandBuffer commands, VkDeviceAddress layoutInfo.pushConstantRangeCount = 1; layoutInfo.pPushConstantRanges = &push; Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &dispatchArgumentLayout), "vkCreatePipelineLayout dispatch arguments"); - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = sizeof(DISPATCH_ARGUMENTS_SPV); - moduleInfo.pCode = DISPATCH_ARGUMENTS_SPV; - Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &module), "vkCreateShaderModule dispatch arguments"); + module = CreateShaderModule(context, DISPATCH_ARGUMENTS_SPV, sizeof(DISPATCH_ARGUMENTS_SPV), "vkCreateShaderModule dispatch arguments"); VkComputePipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; pipelineInfo.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; pipelineInfo.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; @@ -2012,11 +2083,15 @@ bool Recorder::RecordDispatchArguments(VkCommandBuffer commands, VkDeviceAddress VkDeviceAddress source; VkDeviceAddress arguments; std::array limits; - } parameters{source, arguments, {limit[0], limit[1], limit[2], 0u}}; + std::array group; + } parameters{source, arguments, {limit[0], limit[1], limit[2], threadGroup != nullptr ? 1u : 0u}, {1u, 1u, 1u, 0u}}; + if (threadGroup != nullptr) std::copy(threadGroup->begin(), threadGroup->end(), parameters.group.begin()); static_assert(sizeof(parameters) == PushBytes); function(cmdPushConstants, "vkCmdPushConstants")(commands, dispatchArgumentLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, PushBytes, ¶meters); function(cmdDispatch, "vkCmdDispatch")(commands, 1, 1, 1); - recordBarrier(commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_INDIRECT_COMMAND_READ_BIT); + const VkPipelineStageFlags readers = VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT | (threadGroup != nullptr ? VK_PIPELINE_STAGE_TRANSFER_BIT : 0u); + const VkAccessFlags reads = VK_ACCESS_INDIRECT_COMMAND_READ_BIT | (threadGroup != nullptr ? VK_ACCESS_TRANSFER_READ_BIT : 0u); + recordBarrier(commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, readers, VK_ACCESS_SHADER_WRITE_BIT, reads); CountBarriers(CommandClass::DispatchLeading, 2); return true; } @@ -2069,10 +2144,7 @@ bool Recorder::gpuSampleCounter() { layoutInfo.pushConstantRangeCount = 1; layoutInfo.pPushConstantRanges = &push; Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &sampleLayout), "vkCreatePipelineLayout sample counter"); - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = sizeof(SAMPLE_COUNTER_SPV); - moduleInfo.pCode = SAMPLE_COUNTER_SPV; - Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &module), "vkCreateShaderModule sample counter"); + module = CreateShaderModule(context, SAMPLE_COUNTER_SPV, sizeof(SAMPLE_COUNTER_SPV), "vkCreateShaderModule sample counter"); VkComputePipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; pipelineInfo.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; pipelineInfo.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; @@ -2088,9 +2160,7 @@ bool Recorder::gpuSampleCounter() { const VkPushConstantRange dumpPush{VK_SHADER_STAGE_COMPUTE_BIT, 0, SampleDumpPushBytes}; layoutInfo.pPushConstantRanges = &dumpPush; Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &dumpLayout), "vkCreatePipelineLayout sample dumps"); - moduleInfo.codeSize = sizeof(SAMPLE_DUMPS_SPV); - moduleInfo.pCode = SAMPLE_DUMPS_SPV; - Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &module), "vkCreateShaderModule sample dumps"); + module = CreateShaderModule(context, SAMPLE_DUMPS_SPV, sizeof(SAMPLE_DUMPS_SPV), "vkCreateShaderModule sample dumps"); pipelineInfo.stage.module = module; pipelineInfo.layout = dumpLayout; { @@ -2119,6 +2189,56 @@ std::uint64_t Recorder::Batch::StoreRun::Queued::End() const { return address + (dumpTarget != 0 ? SampleDumpBytes : bytes.size()); } +std::vector Recorder::QueuedWriteOrder(std::span writes) { + std::vector order(writes.size()); + for (std::size_t i = 0; i < order.size(); ++i) order[i] = i; + std::vector coveredLater(writes.size()), overLater(writes.size()); + std::map later; + for (auto i = writes.size(); i-- > 0;) { + const auto& write = writes[i]; + if (!write.computed) { + const auto after = later.upper_bound(write.begin); + coveredLater[i] = after != later.begin() && std::prev(after)->second >= write.end; + const auto last = later.lower_bound(write.end); + overLater[i] = last != later.begin() && std::prev(last)->second > write.begin; + continue; + } + auto begin = write.begin, end = write.end; + auto at = later.lower_bound(begin); + if (at != later.begin() && std::prev(at)->second >= begin) --at; + while (at != later.end() && at->first <= end) { + begin = std::min(begin, at->first); + end = std::max(end, at->second); + at = later.erase(at); + } + later.emplace(begin, end); + } + std::vector before, dumps, after; + std::vector> earlier, delayed; + for (std::size_t i = 0; i < writes.size(); ++i) { + const auto& write = writes[i]; + const auto over = [&](const std::vector>& ranges) { + return std::any_of(ranges.begin(), ranges.end(), [&](const auto& range) { return write.begin < range.second && range.first < write.end; }); + }; + if (write.computed) { + dumps.push_back(i); + earlier.emplace_back(write.begin, write.end); + } else if (dumps.empty()) { + before.push_back(i); + } else if (overLater[i]) { + if (over(delayed) || (!coveredLater[i] && over(earlier))) return order; + before.push_back(i); + } else { + after.push_back(i); + delayed.emplace_back(write.begin, write.end); + } + } + order = std::move(before); + order.insert(order.end(), dumps.begin(), dumps.end()); + order.insert(order.end(), after.begin(), after.end()); + return order; +} + std::vector Recorder::QueuedWriteGroups(std::span writes) { std::vector ends; std::vector> computed; @@ -2385,10 +2505,7 @@ bool Recorder::RecordMeshArguments(VkCommandBuffer commands, VkDeviceAddress rec layoutInfo.pushConstantRangeCount = 1; layoutInfo.pPushConstantRanges = &push; Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &meshArgumentLayout), "vkCreatePipelineLayout mesh arguments"); - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = sizeof(MESH_ARGUMENTS_SPV); - moduleInfo.pCode = MESH_ARGUMENTS_SPV; - Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &module), "vkCreateShaderModule mesh arguments"); + module = CreateShaderModule(context, MESH_ARGUMENTS_SPV, sizeof(MESH_ARGUMENTS_SPV), "vkCreateShaderModule mesh arguments"); VkComputePipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; pipelineInfo.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; pipelineInfo.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; @@ -3395,6 +3512,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 @@ -3444,13 +3566,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) { @@ -3473,6 +3605,13 @@ void Recorder::Submit() { newestSubmitted = batch->serial; newestSubmittedAt = batch->submittedAt; } + if (timeline != VK_NULL_HANDLE) { + if (!progressWatcher) { + progressWatcher = std::make_unique(context.device, timeline, context.Function("vkWaitSemaphoresKHR"), + context.Function("vkGetSemaphoreCounterValueKHR")); + } + progressWatcher->Submitted(batch->serial); + } inFlight.push_back(std::move(batch)); inFlightKeptBytes += inFlight.back()->keptBytes; if (activeRecorder == this) pendingLabelSince.store(NoPendingLabel, std::memory_order_release); @@ -3953,11 +4092,22 @@ 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()); } } } + if (!batch->completions.empty()) KernelWakeSleepers_nid_no_patch(); if (profile) holdCounters.completions += batch->completions.size(); const auto keptStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; // Deferred only from inside a hold: the unlock that releases the list is this thread's own, and diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/SampleOrder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/SampleOrder.cpp new file mode 100644 index 0000000000..dfcef91efc --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/SampleOrder.cpp @@ -0,0 +1,672 @@ +#include "prx/libSceAgcDriver/Graphics/include/SampleOrder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" +#include "prx/libSceAgcDriver/Graphics/shaders/SampleOrderDepth_spv.h" +#include "prx/libSceAgcDriver/Graphics/shaders/SampleOrderR16_spv.h" +#include "prx/libSceAgcDriver/Graphics/shaders/SampleOrderR32_spv.h" +#include "prx/libSceAgcDriver/Graphics/shaders/SampleOrderR8_spv.h" +#include "prx/libSceAgcDriver/Graphics/shaders/SampleOrderRG32_spv.h" +#include "prx/libSceAgcDriver/Graphics/shaders/SampleOrderRGBA32_spv.h" +#include "prx/libSceAgcDriver/Graphics/shaders/SampleOrderStencilBits_spv.h" +#include "prx/libSceAgcDriver/Graphics/shaders/SampleOrderStencilExport_spv.h" +#include "prx/libSceAgcDriver/Graphics/shaders/SampleOrderVertex_spv.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver::Graphics { +namespace { + +constexpr VkAccessFlags anyWrite = VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; +constexpr VkAccessFlags anyAccess = anyWrite | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_INPUT_ATTACHMENT_READ_BIT | VK_ACCESS_UNIFORM_READ_BIT | VK_ACCESS_INDEX_READ_BIT | VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT | VK_ACCESS_INDIRECT_COMMAND_READ_BIT; +constexpr VkPipelineStageFlags reorderStages = VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT; +constexpr std::uint32_t setsPerPool = 64; + +struct StoragePush { + std::array map; + std::array extent; + std::uint32_t count; +}; + +struct DepthPush { + std::array map; + std::uint32_t bit; +}; + +bool exactDepth(VkFormat format) { + return format == VK_FORMAT_D16_UNORM || format == VK_FORMAT_D32_SFLOAT || format == VK_FORMAT_D16_UNORM_S8_UINT || format == VK_FORMAT_D32_SFLOAT_S8_UINT; +} + +bool stencilOf(const Context& context, const DepthTarget& depth) { + return depth.stencilAddress != 0 && HasStencil(HostDepthFormat(context, depth.format)); +} + +bool depthSampleable(const Context& context, VkFormat format, std::uint32_t samples) { + if (context.imageFormatProperties == nullptr || context.formatProperties == nullptr) return false; + VkFormatProperties properties{}; + context.formatProperties(context.physical, format, &properties); + if ((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) == 0) return false; + VkImageFormatProperties supported{}; + const VkImageCreateFlags flags = SampleLocationsCompatibleDepth(context, format, samples) ? VK_IMAGE_CREATE_SAMPLE_LOCATIONS_COMPATIBLE_DEPTH_BIT_EXT : 0u; + constexpr VkImageUsageFlags usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + if (context.imageFormatProperties(context.physical, format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage, flags, &supported) != VK_SUCCESS) return false; + return (supported.sampleCounts & samples) != 0; +} + +std::array mapFor(const SampleOrder& order, bool toHost) { + std::array map{}; + for (std::size_t pixel = 0; pixel < map.size(); ++pixel) { + const auto word = order[pixel] == 0u ? KeptSampleOrder : order[pixel]; + map[pixel] = toHost ? word : InvertSampleOrder(word); + } + return map; +} + +VkExtent2D regionOf(const SampleOrderPass& pass, VkExtent2D extent) { + return {std::min(pass.extent.width, extent.width), std::min(pass.extent.height, extent.height)}; +} + +void transitionLocated(const Context& context, VkCommandBuffer commands, VkImage image, VkFormat format, const VkSampleLocationsInfoEXT& locations, VkImageLayout from, VkImageLayout to, VkPipelineStageFlags sourceStages, VkAccessFlags sourceAccess, VkPipelineStageFlags destinationStages, VkAccessFlags destinationAccess) { + VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, &locations}; + barrier.srcAccessMask = sourceAccess; + barrier.dstAccessMask = destinationAccess; + barrier.oldLayout = from; + barrier.newLayout = to; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = image; + barrier.subresourceRange = {VK_IMAGE_ASPECT_DEPTH_BIT | (HasStencil(format) ? VK_IMAGE_ASPECT_STENCIL_BIT : 0u), 0, 1, 0, 1}; + context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, sourceStages, destinationStages, 0, 0, nullptr, 0, nullptr, 1, &barrier); +} + +class CopyImage { +public: + CopyImage(const Context& context, VkFormat format, std::uint32_t samples, VkExtent2D extent, bool stencil) : context(context), format(format), samples(samples), extent(extent) { + try { + VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + info.imageType = VK_IMAGE_TYPE_2D; + info.format = format; + info.extent = {extent.width, extent.height, 1}; + info.mipLevels = 1; + info.arrayLayers = 1; + info.samples = static_cast(samples); + info.tiling = VK_IMAGE_TILING_OPTIMAL; + info.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + Check(context.Function("vkCreateImage")(context.device, &info, nullptr, &image), "vkCreateImage sample order copy"); + VkMemoryRequirements requirements{}; + context.Function("vkGetImageMemoryRequirements")(context.device, image, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; + allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory sample order copy"); + Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory sample order copy"); + depthView = createView(VK_IMAGE_ASPECT_DEPTH_BIT); + if (stencil) stencilView = createView(VK_IMAGE_ASPECT_STENCIL_BIT); + } catch (...) { + release(); + throw; + } + } + ~CopyImage() { release(); } + CopyImage(const CopyImage&) = delete; + CopyImage& operator=(const CopyImage&) = delete; + + Context context; + VkFormat format; + std::uint32_t samples; + VkExtent2D extent; + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + VkImageView depthView = VK_NULL_HANDLE; + VkImageView stencilView = VK_NULL_HANDLE; + bool general = false; + +private: + VkImageView createView(VkImageAspectFlags aspect) { + VkImageViewCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + info.image = image; + info.viewType = VK_IMAGE_VIEW_TYPE_2D; + info.format = format; + info.subresourceRange = {aspect, 0, 1, 0, 1}; + VkImageView view = VK_NULL_HANDLE; + Check(context.Function("vkCreateImageView")(context.device, &info, nullptr, &view), "vkCreateImageView sample order copy"); + return view; + } + + void release() noexcept { + if (stencilView) context.Function("vkDestroyImageView")(context.device, stencilView, nullptr); + if (depthView) context.Function("vkDestroyImageView")(context.device, depthView, nullptr); + if (image) context.Function("vkDestroyImage")(context.device, image, nullptr); + if (memory) context.Function("vkFreeMemory")(context.device, memory, nullptr); + } +}; + +class SampleOrders { +public: + explicit SampleOrders(const Context& context) : context(context) { + this->context.bufferPool.reset(); + } + ~SampleOrders() { release(); } + SampleOrders(const SampleOrders&) = delete; + SampleOrders& operator=(const SampleOrders&) = delete; + + void Storage(VkCommandBuffer commands, const SampleOrderTarget& target, const std::array& map, VkExtent2D region) { + const auto pipeline = storagePipeline(target.format); + const auto set = allocate(storageSetLayout, {{VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, setsPerPool}}); + const VkDescriptorImageInfo imageInfo{VK_NULL_HANDLE, target.view, VK_IMAGE_LAYOUT_GENERAL}; + VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; + write.dstSet = set; + write.dstBinding = 0; + write.descriptorCount = 1; + write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; + write.pImageInfo = &imageInfo; + context.Function("vkUpdateDescriptorSets")(context.device, 1, &write, 0, nullptr); + const StoragePush push{map, {region.width, region.height}, target.samples}; + context.Resolved(&DeviceFunctions::cmdBindPipeline, "vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline); + context.Resolved(&DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, storagePipelineLayout, 0, 1, &set, 0, nullptr); + context.Resolved(&DeviceFunctions::cmdPushConstants, "vkCmdPushConstants")(commands, storagePipelineLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(push), &push); + context.Resolved(&DeviceFunctions::cmdDispatch, "vkCmdDispatch")(commands, ((region.width + 1u) / 2u + 7u) / 8u, ((region.height + 1u) / 2u + 7u) / 8u, 1); + } + + void Depth(VkCommandBuffer commands, const SampleOrderTarget& target, const std::array& map, VkExtent2D region, const SampleOrderPass& order, std::vector>& kept) { + const bool stencil = target.method != SampleOrderMethod::Depth; + auto copy = copyFor(target.format, target.samples, target.extent, stencil); + kept.push_back(copy); + const VkImageAspectFlags aspects = VK_IMAGE_ASPECT_DEPTH_BIT | (stencil ? VK_IMAGE_ASPECT_STENCIL_BIT : 0u); + if (!copy->general) { + VkImageMemoryBarrier toGeneral{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + toGeneral.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toGeneral.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + toGeneral.newLayout = VK_IMAGE_LAYOUT_GENERAL; + toGeneral.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toGeneral.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toGeneral.image = copy->image; + toGeneral.subresourceRange = {aspects, 0, 1, 0, 1}; + context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &toGeneral); + copy->general = true; + } + VkImageCopy region3d{}; + region3d.srcSubresource = {aspects, 0, 0, 1}; + region3d.dstSubresource = {aspects, 0, 0, 1}; + region3d.extent = {region.width, region.height, 1}; + const auto& locations = order.locations; + const bool located = order.located; + const VkSampleLocationsInfoEXT info{VK_STRUCTURE_TYPE_SAMPLE_LOCATIONS_INFO_EXT, nullptr, static_cast(target.samples), {1, 1}, target.samples, locations.data()}; + constexpr VkPipelineStageFlags fragmentStages = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT; + constexpr VkAccessFlags depthAccess = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + if (located) transitionLocated(context, commands, target.image, target.format, info, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, anyWrite, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_READ_BIT); + context.Function("vkCmdCopyImage")(commands, target.image, located ? VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL : VK_IMAGE_LAYOUT_GENERAL, copy->image, VK_IMAGE_LAYOUT_GENERAL, 1, ®ion3d); + if (located) transitionLocated(context, commands, target.image, target.format, info, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, fragmentStages, depthAccess); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, fragmentStages, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | depthAccess); + const auto& pass = depthPass(target.format, target.samples, target.method, located, locations); + const auto set = allocate(sampledSetLayout, {{VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, setsPerPool * 2u}}); + std::array images{{{VK_NULL_HANDLE, copy->depthView, VK_IMAGE_LAYOUT_GENERAL}, {VK_NULL_HANDLE, stencil ? copy->stencilView : copy->depthView, VK_IMAGE_LAYOUT_GENERAL}}}; + std::array writes{}; + for (std::uint32_t binding = 0; binding < writes.size(); ++binding) { + writes[binding].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[binding].dstSet = set; + writes[binding].dstBinding = binding; + writes[binding].descriptorCount = 1; + writes[binding].descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; + writes[binding].pImageInfo = &images[binding]; + } + context.Function("vkUpdateDescriptorSets")(context.device, stencil ? 2u : 1u, writes.data(), 0, nullptr); + const std::array views{target.view}; + auto framebuffer = std::make_shared(context, pass.renderPass, views, region); + kept.push_back(framebuffer); + VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; + begin.renderPass = pass.renderPass; + begin.framebuffer = framebuffer->Handle(); + begin.renderArea = {{0, 0}, region}; + const VkAttachmentSampleLocationsEXT initial{0, info}; + const VkSubpassSampleLocationsEXT post{0, info}; + VkRenderPassSampleLocationsBeginInfoEXT sampleLocations{VK_STRUCTURE_TYPE_RENDER_PASS_SAMPLE_LOCATIONS_BEGIN_INFO_EXT}; + sampleLocations.attachmentInitialSampleLocationsCount = 1; + sampleLocations.pAttachmentInitialSampleLocations = &initial; + sampleLocations.postSubpassSampleLocationsCount = 1; + sampleLocations.pPostSubpassSampleLocations = &post; + if (located) begin.pNext = &sampleLocations; + context.Resolved(&DeviceFunctions::cmdBeginRenderPass, "vkCmdBeginRenderPass")(commands, &begin, VK_SUBPASS_CONTENTS_INLINE); + const VkViewport viewport{0.0f, 0.0f, static_cast(region.width), static_cast(region.height), 0.0f, 1.0f}; + const VkRect2D scissor{{0, 0}, region}; + context.Resolved(&DeviceFunctions::cmdSetViewport, "vkCmdSetViewport")(commands, 0, 1, &viewport); + context.Resolved(&DeviceFunctions::cmdSetScissor, "vkCmdSetScissor")(commands, 0, 1, &scissor); + context.Resolved(&DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, sampledPipelineLayout, 0, 1, &set, 0, nullptr); + const auto draw = [&](VkPipeline pipeline, std::uint32_t bit, std::uint32_t writeMask, std::uint32_t reference) { + context.Resolved(&DeviceFunctions::cmdBindPipeline, "vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); + context.Resolved(&DeviceFunctions::cmdSetStencilWriteMask, "vkCmdSetStencilWriteMask")(commands, VK_STENCIL_FACE_FRONT_AND_BACK, writeMask); + context.Resolved(&DeviceFunctions::cmdSetStencilReference, "vkCmdSetStencilReference")(commands, VK_STENCIL_FACE_FRONT_AND_BACK, reference); + const DepthPush push{map, bit}; + context.Resolved(&DeviceFunctions::cmdPushConstants, "vkCmdPushConstants")(commands, sampledPipelineLayout, VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(push), &push); + context.Resolved(&DeviceFunctions::cmdDraw, "vkCmdDraw")(commands, 3, 1, 0, 0); + }; + draw(pass.depth, 0u, 0xffu, 0u); + if (target.method == SampleOrderMethod::DepthStencilBits) { + for (std::uint32_t bit = 0; bit < 8u; ++bit) draw(pass.stencilBits, bit, 1u << bit, 0xffu); + } + context.Resolved(&DeviceFunctions::cmdEndRenderPass, "vkCmdEndRenderPass")(commands); + } + + Context context; + +private: + struct Ring { + std::vector pools; + std::vector serials; + std::size_t current = 0; + std::uint32_t used = 0; + }; + + struct DepthPass { + VkRenderPass renderPass = VK_NULL_HANDLE; + VkPipeline depth = VK_NULL_HANDLE; + VkPipeline stencilBits = VK_NULL_HANDLE; + }; + + using DepthKey = std::tuple>; + using CopyKey = std::tuple; + + VkShaderModule module(const std::uint32_t* code, std::size_t bytes) { + VkShaderModuleCreateInfo info{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + info.codeSize = bytes; + info.pCode = code; + VkShaderModule created = VK_NULL_HANDLE; + Check(context.Function("vkCreateShaderModule")(context.device, &info, nullptr, &created), "vkCreateShaderModule sample order"); + return created; + } + + void destroyModule(VkShaderModule shader) { + context.Function("vkDestroyShaderModule")(context.device, shader, nullptr); + } + + VkDescriptorSetLayout setLayout(VkDescriptorType type, std::uint32_t count, VkShaderStageFlags stage) { + std::array bindings{}; + for (std::uint32_t binding = 0; binding < count; ++binding) bindings[binding] = {binding, type, 1, stage, nullptr}; + VkDescriptorSetLayoutCreateInfo info{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; + info.bindingCount = count; + info.pBindings = bindings.data(); + VkDescriptorSetLayout layout = VK_NULL_HANDLE; + Check(context.Function("vkCreateDescriptorSetLayout")(context.device, &info, nullptr, &layout), "vkCreateDescriptorSetLayout sample order"); + return layout; + } + + VkPipelineLayout pipelineLayout(VkDescriptorSetLayout set, VkShaderStageFlags stage, std::uint32_t pushBytes) { + const VkPushConstantRange range{stage, 0, pushBytes}; + VkPipelineLayoutCreateInfo info{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + info.setLayoutCount = 1; + info.pSetLayouts = &set; + info.pushConstantRangeCount = 1; + info.pPushConstantRanges = ⦥ + VkPipelineLayout layout = VK_NULL_HANDLE; + Check(context.Function("vkCreatePipelineLayout")(context.device, &info, nullptr, &layout), "vkCreatePipelineLayout sample order"); + return layout; + } + + VkPipeline storagePipeline(VkFormat format) { + const std::array formats{VK_FORMAT_R8_UINT, VK_FORMAT_R16_UINT, VK_FORMAT_R32_UINT, VK_FORMAT_R32G32_UINT, VK_FORMAT_R32G32B32A32_UINT}; + const auto found = std::ranges::find(formats, format); + Require(found != formats.end(), "reordering the samples of a color target through a storage view of this format is not implemented"); + const auto index = static_cast(found - formats.begin()); + if (storagePipelines[index] != VK_NULL_HANDLE) return storagePipelines[index]; + if (storageSetLayout == VK_NULL_HANDLE) storageSetLayout = setLayout(VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1, VK_SHADER_STAGE_COMPUTE_BIT); + if (storagePipelineLayout == VK_NULL_HANDLE) storagePipelineLayout = pipelineLayout(storageSetLayout, VK_SHADER_STAGE_COMPUTE_BIT, sizeof(StoragePush)); + const std::array, 5> code{{{SAMPLE_ORDER_R8_SPV, sizeof(SAMPLE_ORDER_R8_SPV)}, {SAMPLE_ORDER_R16_SPV, sizeof(SAMPLE_ORDER_R16_SPV)}, {SAMPLE_ORDER_R32_SPV, sizeof(SAMPLE_ORDER_R32_SPV)}, {SAMPLE_ORDER_RG32_SPV, sizeof(SAMPLE_ORDER_RG32_SPV)}, {SAMPLE_ORDER_RGBA32_SPV, sizeof(SAMPLE_ORDER_RGBA32_SPV)}}}; + const auto shader = module(code[index].first, code[index].second); + VkComputePipelineCreateInfo info{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; + info.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; + info.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; + info.stage.module = shader; + info.stage.pName = "main"; + info.layout = storagePipelineLayout; + VkResult created; + { + auto pipelineCacheLock = context.LockPipelineCacheForUse(); + created = context.Function("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &info, nullptr, &storagePipelines[index]); + } + destroyModule(shader); + Check(created, "vkCreateComputePipelines sample order"); + return storagePipelines[index]; + } + + const DepthPass& depthPass(VkFormat format, std::uint32_t samples, SampleOrderMethod method, bool located, const std::array& locations) { + std::array standIn{}; + if (located) { + for (std::uint32_t sample = 0; sample < samples && sample < 8u; ++sample) { + standIn[sample * 2u] = std::bit_cast(locations[sample].x); + standIn[sample * 2u + 1u] = std::bit_cast(locations[sample].y); + } + } + const DepthKey key{format, samples, method, located, standIn}; + if (const auto found = depthPasses.find(key); found != depthPasses.end()) return found->second; + if (sampledSetLayout == VK_NULL_HANDLE) sampledSetLayout = setLayout(VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 2, VK_SHADER_STAGE_FRAGMENT_BIT); + if (sampledPipelineLayout == VK_NULL_HANDLE) sampledPipelineLayout = pipelineLayout(sampledSetLayout, VK_SHADER_STAGE_FRAGMENT_BIT, sizeof(DepthPush)); + const bool stencil = method != SampleOrderMethod::Depth; + DepthPass pass; + try { + VkAttachmentDescription attachment{}; + attachment.format = format; + attachment.samples = static_cast(samples); + attachment.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; + attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + attachment.stencilLoadOp = stencil ? VK_ATTACHMENT_LOAD_OP_LOAD : VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachment.stencilStoreOp = stencil ? VK_ATTACHMENT_STORE_OP_STORE : VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachment.initialLayout = VK_IMAGE_LAYOUT_GENERAL; + attachment.finalLayout = VK_IMAGE_LAYOUT_GENERAL; + if (HasStencil(format) && !stencil) { + attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_LOAD; + attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_STORE; + } + const VkAttachmentReference reference{0, VK_IMAGE_LAYOUT_GENERAL}; + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.pDepthStencilAttachment = &reference; + VkRenderPassCreateInfo passInfo{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; + passInfo.attachmentCount = 1; + passInfo.pAttachments = &attachment; + passInfo.subpassCount = 1; + passInfo.pSubpasses = &subpass; + Check(context.Function("vkCreateRenderPass")(context.device, &passInfo, nullptr, &pass.renderPass), "vkCreateRenderPass sample order"); + const auto create = [&](const std::uint32_t* code, std::size_t bytes, bool writesDepth) { + const auto vertex = module(SAMPLE_ORDER_VERTEX_SPV, sizeof(SAMPLE_ORDER_VERTEX_SPV)); + const auto fragment = module(code, bytes); + std::array stages{}; + stages[0] = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, nullptr, 0, VK_SHADER_STAGE_VERTEX_BIT, vertex, "main", nullptr}; + stages[1] = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, nullptr, 0, VK_SHADER_STAGE_FRAGMENT_BIT, fragment, "main", nullptr}; + VkPipelineVertexInputStateCreateInfo input{VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO}; + VkPipelineInputAssemblyStateCreateInfo assembly{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO}; + assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + VkPipelineViewportStateCreateInfo viewports{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO}; + viewports.viewportCount = 1; + viewports.scissorCount = 1; + VkPipelineRasterizationStateCreateInfo raster{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO}; + raster.polygonMode = VK_POLYGON_MODE_FILL; + raster.cullMode = VK_CULL_MODE_NONE; + raster.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + raster.lineWidth = 1.0f; + VkPipelineMultisampleStateCreateInfo multisample{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO}; + multisample.rasterizationSamples = static_cast(samples); + multisample.sampleShadingEnable = VK_TRUE; + multisample.minSampleShading = 1.0f; + VkPipelineSampleLocationsStateCreateInfoEXT sampleLocations{VK_STRUCTURE_TYPE_PIPELINE_SAMPLE_LOCATIONS_STATE_CREATE_INFO_EXT}; + sampleLocations.sampleLocationsEnable = VK_TRUE; + sampleLocations.sampleLocationsInfo = {VK_STRUCTURE_TYPE_SAMPLE_LOCATIONS_INFO_EXT, nullptr, static_cast(samples), {1, 1}, samples, locations.data()}; + if (located) multisample.pNext = &sampleLocations; + VkStencilOpState replace{}; + replace.failOp = VK_STENCIL_OP_REPLACE; + replace.passOp = VK_STENCIL_OP_REPLACE; + replace.depthFailOp = VK_STENCIL_OP_REPLACE; + replace.compareOp = VK_COMPARE_OP_ALWAYS; + replace.compareMask = 0xffu; + replace.writeMask = 0xffu; + VkPipelineDepthStencilStateCreateInfo depthStencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO}; + depthStencil.depthTestEnable = writesDepth ? VK_TRUE : VK_FALSE; + depthStencil.depthWriteEnable = writesDepth ? VK_TRUE : VK_FALSE; + depthStencil.depthCompareOp = VK_COMPARE_OP_ALWAYS; + depthStencil.stencilTestEnable = stencil ? VK_TRUE : VK_FALSE; + depthStencil.front = replace; + depthStencil.back = replace; + const std::array dynamicStates{VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR, VK_DYNAMIC_STATE_STENCIL_WRITE_MASK, VK_DYNAMIC_STATE_STENCIL_REFERENCE}; + VkPipelineDynamicStateCreateInfo dynamic{VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO}; + dynamic.dynamicStateCount = static_cast(dynamicStates.size()); + dynamic.pDynamicStates = dynamicStates.data(); + VkGraphicsPipelineCreateInfo info{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO}; + info.stageCount = static_cast(stages.size()); + info.pStages = stages.data(); + info.pVertexInputState = &input; + info.pInputAssemblyState = &assembly; + info.pViewportState = &viewports; + info.pRasterizationState = &raster; + info.pMultisampleState = &multisample; + info.pDepthStencilState = &depthStencil; + info.pDynamicState = &dynamic; + info.layout = sampledPipelineLayout; + info.renderPass = pass.renderPass; + VkPipeline pipeline = VK_NULL_HANDLE; + VkResult created; + { + auto pipelineCacheLock = context.LockPipelineCacheForUse(); + created = context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &info, nullptr, &pipeline); + } + destroyModule(fragment); + destroyModule(vertex); + Check(created, "vkCreateGraphicsPipelines sample order"); + return pipeline; + }; + if (method == SampleOrderMethod::DepthStencilExport) pass.depth = create(SAMPLE_ORDER_STENCILEXPORT_SPV, sizeof(SAMPLE_ORDER_STENCILEXPORT_SPV), true); + else pass.depth = create(SAMPLE_ORDER_DEPTH_SPV, sizeof(SAMPLE_ORDER_DEPTH_SPV), true); + if (method == SampleOrderMethod::DepthStencilBits) pass.stencilBits = create(SAMPLE_ORDER_STENCILBITS_SPV, sizeof(SAMPLE_ORDER_STENCILBITS_SPV), false); + } catch (...) { + destroyPass(pass); + throw; + } + return depthPasses.emplace(key, pass).first->second; + } + + std::shared_ptr copyFor(VkFormat format, std::uint32_t samples, VkExtent2D extent, bool stencil) { + const CopyKey key{format, samples, extent.width, extent.height, stencil}; + if (const auto found = copies.find(key); found != copies.end()) { + found->second.second = ++uses; + return found->second.first; + } + constexpr std::size_t bound = 4; + while (copies.size() >= bound) { + const auto oldest = std::ranges::min_element(copies, [](const auto& a, const auto& b) { return a.second.second < b.second.second; }); + copies.erase(oldest); + } + auto copy = std::make_shared(context, format, samples, extent, stencil); + copies.emplace(key, std::pair{copy, ++uses}); + return copy; + } + + VkDescriptorSet allocate(VkDescriptorSetLayout layout, const std::vector& sizes) { + auto& ring = layout == storageSetLayout ? storageRing : sampledRing; + const auto* recorder = Recorder::Active(); + if (ring.pools.empty() || ring.used == setsPerPool) { + const auto completed = recorder != nullptr ? recorder->CompletedThrough() : 0; + auto next = ring.pools.size(); + for (std::size_t step = 1; step <= ring.pools.size(); ++step) { + const auto index = (ring.current + step) % ring.pools.size(); + if (ring.serials[index] <= completed) { + next = index; + break; + } + } + if (next < ring.pools.size()) { + Check(context.Function("vkResetDescriptorPool")(context.device, ring.pools[next], 0), "vkResetDescriptorPool sample order"); + } else { + VkDescriptorPoolCreateInfo info{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; + info.maxSets = setsPerPool; + info.poolSizeCount = static_cast(sizes.size()); + info.pPoolSizes = sizes.data(); + VkDescriptorPool pool = VK_NULL_HANDLE; + Check(context.Function("vkCreateDescriptorPool")(context.device, &info, nullptr, &pool), "vkCreateDescriptorPool sample order"); + ring.pools.push_back(pool); + ring.serials.push_back(0); + } + ring.current = next; + ring.used = 0; + } + VkDescriptorSetAllocateInfo allocation{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; + allocation.descriptorPool = ring.pools[ring.current]; + allocation.descriptorSetCount = 1; + allocation.pSetLayouts = &layout; + VkDescriptorSet set = VK_NULL_HANDLE; + Check(context.Function("vkAllocateDescriptorSets")(context.device, &allocation, &set), "vkAllocateDescriptorSets sample order"); + ++ring.used; + ring.serials[ring.current] = recorder != nullptr && recorder->Device() == context.device ? recorder->OpenSerial() : std::numeric_limits::max(); + return set; + } + + void destroyPass(const DepthPass& pass) noexcept { + if (pass.stencilBits) context.Function("vkDestroyPipeline")(context.device, pass.stencilBits, nullptr); + if (pass.depth) context.Function("vkDestroyPipeline")(context.device, pass.depth, nullptr); + if (pass.renderPass) context.Function("vkDestroyRenderPass")(context.device, pass.renderPass, nullptr); + } + + void release() noexcept { + copies.clear(); + for (const auto& [key, pass] : depthPasses) destroyPass(pass); + depthPasses.clear(); + for (const auto pipeline : storagePipelines) { + if (pipeline) context.Function("vkDestroyPipeline")(context.device, pipeline, nullptr); + } + for (auto* ring : {&storageRing, &sampledRing}) { + for (const auto pool : ring->pools) context.Function("vkDestroyDescriptorPool")(context.device, pool, nullptr); + ring->pools.clear(); + } + for (const auto layout : {storagePipelineLayout, sampledPipelineLayout}) { + if (layout) context.Function("vkDestroyPipelineLayout")(context.device, layout, nullptr); + } + for (const auto layout : {storageSetLayout, sampledSetLayout}) { + if (layout) context.Function("vkDestroyDescriptorSetLayout")(context.device, layout, nullptr); + } + } + + VkDescriptorSetLayout storageSetLayout = VK_NULL_HANDLE; + VkDescriptorSetLayout sampledSetLayout = VK_NULL_HANDLE; + VkPipelineLayout storagePipelineLayout = VK_NULL_HANDLE; + VkPipelineLayout sampledPipelineLayout = VK_NULL_HANDLE; + std::array storagePipelines{}; + std::map depthPasses; + std::map, std::uint64_t>> copies; + std::uint64_t uses = 0; + Ring storageRing; + Ring sampledRing; +}; + +std::mutex& ordersMutex() { + static std::mutex mutex; + return mutex; +} + +std::vector>& orders() { + static std::vector> list; + return list; +} + +SampleOrders& ordersFor(const Context& context) { + std::lock_guard lock(ordersMutex()); + for (const auto& entry : orders()) { + if (entry->context.device == context.device) return *entry; + } + orders().push_back(std::make_unique(context)); + return *orders().back(); +} + +} + +SampleOrderMethod DepthSampleOrderMethod(const Context& context, const DepthTarget& depth) { + if (!stencilOf(context, depth)) return SampleOrderMethod::Depth; + return context.shaderStencilExport ? SampleOrderMethod::DepthStencilExport : SampleOrderMethod::DepthStencilBits; +} + +bool HostsSampleLocationGrid(VkExtent2D largestGrid) { + return largestGrid.width != 0u && largestGrid.height != 0u && largestGrid.width % 2u == 0u && largestGrid.height % 2u == 0u; +} + +bool MayReorderSamples(const Context& context, std::uint32_t samples) { + return samples > 1u && (context.sampleLocationSampleCounts & samples) != 0 && (context.sampleLocationGridSampleCounts & samples) == 0; +} + +VkFormat SampleOrderStorageFormat(const Context& context, VkFormat format, std::uint32_t elementBytes, std::uint32_t samples) { + if (!context.storageImageMultisample || context.imageFormatProperties == nullptr || samples <= 1u) return VK_FORMAT_UNDEFINED; + const auto alias = TextureDetiler::ImageElementFormat(elementBytes); + if (alias == VK_FORMAT_UNDEFINED) return VK_FORMAT_UNDEFINED; + VkImageFormatProperties supported{}; + if (context.imageFormatProperties(context.physical, alias, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_STORAGE_BIT, 0, &supported) != VK_SUCCESS || (supported.sampleCounts & samples) == 0) return VK_FORMAT_UNDEFINED; + constexpr VkImageUsageFlags usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_STORAGE_BIT; + constexpr VkImageCreateFlags flags = VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | VK_IMAGE_CREATE_EXTENDED_USAGE_BIT; + if (context.imageFormatProperties(context.physical, format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage, flags, &supported) != VK_SUCCESS || (supported.sampleCounts & samples) == 0) return VK_FORMAT_UNDEFINED; + return alias; +} + +std::string SampleOrderRefusal(const Context& context, const State& state) { + if (!context.sampleRateShading) return "the device lacks sampleRateShading"; + if (!context.storageImageMultisample) return "the device lacks shaderStorageImageMultisample"; + for (const auto& color : state.colors) { + if (color.samples > 1u && SampleOrderStorageFormat(context, color.format, color.elementBytes, color.samples) == VK_FORMAT_UNDEFINED) return "color format " + std::to_string(color.format) + " has no storage view at " + std::to_string(color.samples) + " samples"; + } + if (state.depth && state.depth->samples > 1u) { + const auto host = HostDepthFormat(context, state.depth->format); + if (!exactDepth(host)) return "depth format " + std::to_string(host) + " is not written back exactly through FragDepth"; + if (!depthSampleable(context, host, state.depth->samples)) return "depth format " + std::to_string(host) + " cannot be sampled at " + std::to_string(state.depth->samples) + " samples"; + } + return {}; +} + +HostSampleLocations HostSampleLocationsFor(const Context& context, const State& state) { + if (!state.sampleLocationGrid || (context.sampleLocationGridSampleCounts & state.samples) != 0) return {SampleLocationGrid(state), {}}; + const auto order = SameSetSampleOrder(state); + const auto refusal = order ? SampleOrderRefusal(context, state) : std::string("its pixels hold different locations"); + static std::atomic reported{0}; + const auto bit = refusal.empty() ? state.samples : state.samples << 8u; + if ((reported.fetch_or(bit, std::memory_order_relaxed) & bit) == 0) { + if (refusal.empty()) std::fprintf(stderr, "[gpu] no 2x2 sample location grid at sample count %u on this device: pixel (0,0)'s sample locations stand in for every pixel, and each pixel's samples are put in the guest's order at the bounds of its render passes\n", state.samples); + else std::fprintf(stderr, "[gpu] no 2x2 sample location grid at sample count %u on this device: sample locations varying between pixels take pixel (0,0)'s in every pixel (%s)\n", state.samples, refusal.c_str()); + } + return {{1, 1}, refusal.empty() ? *order : SampleOrder{}}; +} + +std::vector PlanSampleOrder(const Context& context, const State& state, const SampleOrder& order, VkAttachmentLoadOp load, VkAttachmentStoreOp store) { + std::vector steps; + if (!ReordersSamples(order)) return steps; + const bool toHost = load == VK_ATTACHMENT_LOAD_OP_LOAD; + const bool toGuest = store == VK_ATTACHMENT_STORE_OP_STORE; + if (!toHost && !toGuest) return steps; + for (std::uint32_t index = 0; index < state.colors.size(); ++index) { + if (state.colors[index].samples > 1u) steps.push_back({index, SampleOrderMethod::Storage, toHost, toGuest}); + } + if (state.depth && state.depth->samples > 1u) steps.push_back({static_cast(state.colors.size()), DepthSampleOrderMethod(context, *state.depth), toHost, toGuest}); + return steps; +} + +SampleOrderPass SampleOrderPassFor(const Context& context, const State& state, const SampleOrder& order) { + return {order, state.renderExtent, SampleLocations(state), ReordersSamples(order) && ValidateSampleLocations(context, state)}; +} + +void RecordSampleOrder(const Context& context, VkCommandBuffer commands, const SampleOrderPass& pass, std::span targets, bool toHost, std::vector>& kept) { + if (targets.empty() || !ReordersSamples(pass.order)) return; + auto& reorder = ordersFor(context); + const auto map = mapFor(pass.order, toHost); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, reorderStages, anyWrite, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT); + for (const auto& target : targets) { + Require(target.image != VK_NULL_HANDLE && target.view != VK_NULL_HANDLE, "a reordered attachment has no image"); + const auto region = regionOf(pass, target.extent); + if (region.width == 0 || region.height == 0) continue; + if (target.method == SampleOrderMethod::Storage) reorder.Storage(commands, target, map, region); + else reorder.Depth(commands, target, map, region, pass, kept); + if (target.keep != nullptr) kept.push_back(target.keep); + } + RecordMemoryBarrier(context, commands, reorderStages, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, anyAccess); + if (Recorder::Active() != nullptr) Recorder::CountBarriers(Recorder::CommandClass::Draw, 2); +} + +void RecordDepthSampleLocations(const Context& context, VkCommandBuffer commands, const DepthSampleLocations& depth) { + Require(depth.image != VK_NULL_HANDLE && depth.samples * depth.grid.width * depth.grid.height <= depth.locations.size(), "a depth target drawn at custom sample locations has no image or too many locations"); + const VkSampleLocationsInfoEXT info{VK_STRUCTURE_TYPE_SAMPLE_LOCATIONS_INFO_EXT, nullptr, static_cast(depth.samples), depth.grid, depth.samples * depth.grid.width * depth.grid.height, depth.locations.data()}; + transitionLocated(context, commands, depth.image, depth.format, info, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, anyWrite, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_READ_BIT); + transitionLocated(context, commands, depth.image, depth.format, info, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, anyAccess); +} + +std::array, 9> SampleOrderShaderModules() { + return {SAMPLE_ORDER_R8_SPV, SAMPLE_ORDER_R16_SPV, SAMPLE_ORDER_R32_SPV, SAMPLE_ORDER_RG32_SPV, SAMPLE_ORDER_RGBA32_SPV, SAMPLE_ORDER_VERTEX_SPV, SAMPLE_ORDER_DEPTH_SPV, SAMPLE_ORDER_STENCILEXPORT_SPV, SAMPLE_ORDER_STENCILBITS_SPV}; +} + +void ClearSampleOrders(VkDevice device) { + std::lock_guard lock(ordersMutex()); + std::erase_if(orders(), [&](const auto& entry) { return entry->context.device == device; }); +} + +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp index a2ecdd9769..d7b1fb0056 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp @@ -124,7 +124,7 @@ namespace AgcDriver::Graphics { } bool ForcesDegammaOn(std::uint32_t guestFormat, std::uint32_t samplerMask, std::span> samplers) { - if (IsSrgbTextureFormat(guestFormat)) return false; + if (IsSrgbTextureFormat(guestFormat) || IgnoresForcedDegamma(guestFormat)) return false; for (std::uint32_t element = 0; element < 32u && element < samplers.size(); ++element) { if (((samplerMask >> element) & 1u) != 0u && samplers[element]->ForcesDegamma()) return true; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp index 8ec1d42180..3859fc17d0 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp @@ -143,7 +143,9 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con const bool pixelKillEnable = ((shaderControl >> 6u) & 0x1u) != 0; const bool depthExportEnable = (shaderControl & 0x1u) != 0; const bool sampleMaskExportEnable = ((shaderControl >> 8u) & 0x1u) != 0; - const bool stencilRefExportEnable = ((shaderControl >> 1u) & 0x1u) != 0; + const bool stencilOperationValue = !nullProgram && StencilOperationValueReplaces(context); + const bool stencilRefExportEnable = ((shaderControl >> 1u) & 0x1u) != 0 || stencilOperationValue; + if (stencilOperationValue && PixelEarlyZ(shaderControl)) throw std::runtime_error("AGC graphics: early tests with a pixel shader that exports the stencil operation value are unsupported"); const auto loaded = [&](PixelInput input) { return (activeInputs & PixelInputBit(input)) != 0; }; std::uint32_t positionControl = 0; if (loaded(PixelInput::PositionX) || loaded(PixelInput::PositionY)) { @@ -181,6 +183,7 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con .targetOutputMode = targetOutputMode, .targetExportMapping = nullProgram ? std::array{} : exportMappings, .stencilRefExportEnable = stencilRefExportEnable, + .stencilOperationValueExport = stencilOperationValue, .positionLocation = (positionControl & 0x100000u) != 0u ? ShaderRecompiler::PixelPositionLocation::UpperLeft : static_cast(floatLocation) }; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 75ab4242bc..277eea93c5 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" @@ -119,6 +120,7 @@ struct CachedTexture { // The untrackedKey the snapshot was last read or compared equal at, for a surface outside the // write watch; zero when none was taken. std::array untracked{}; + std::uint64_t surfaceBytes = 0; }; // Entries in use order (front = most recent) with a hash index by key: a lookup is O(1) and the @@ -274,16 +276,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 +309,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 { @@ -418,20 +421,46 @@ struct LookupRecord { }; thread_local std::vector* lookupLogSlot = nullptr; +constexpr std::size_t LookupLogBound = 1024; +constexpr std::size_t NoBuildLookups = std::numeric_limits::max(); +thread_local std::size_t buildLookupsStart = NoBuildLookups; +thread_local bool buildLookupsPastBound = false; +std::atomic lookupBuildsPastBound{0}; std::vector& lookupLog() { return ShaderRecompiler::ThreadOwned(lookupLogSlot); } void logLookup(const LookupRecord& record) { - // The bound drops the oldest half, never everything: one build's lookups (a few dozen at most) - // are the newest records, and a capture must find them even when the bound trips mid-build. - constexpr std::size_t bound = 1024; auto& log = lookupLog(); - if (log.size() >= bound) log.erase(log.begin(), log.begin() + bound / 2); + if (log.size() >= LookupLogBound) { + const auto dropped = std::min(LookupLogBound / 2, buildLookupsStart); + if (dropped < LookupLogBound / 2) buildLookupsPastBound = true; + log.erase(log.begin(), log.begin() + static_cast(dropped)); + if (buildLookupsStart != NoBuildLookups) buildLookupsStart -= dropped; + } log.push_back(record); } +class BuildLookups { +public: + BuildLookups() : outermost(buildLookupsStart == NoBuildLookups) { buildLookupsStart = std::min(buildLookupsStart, lookupLog().size()); } + ~BuildLookups() { + if (!outermost) return; + buildLookupsStart = NoBuildLookups; + if (buildLookupsPastBound) lookupBuildsPastBound.fetch_add(1, std::memory_order_relaxed); + buildLookupsPastBound = false; + auto& log = lookupLog(); + if (log.size() > LookupLogBound) log.erase(log.begin(), log.end() - static_cast(LookupLogBound)); + if (log.capacity() > LookupLogBound + LookupLogBound / 2) log.shrink_to_fit(); + } + BuildLookups(const BuildLookups&) = delete; + BuildLookups& operator=(const BuildLookups&) = delete; + +private: + bool outermost; +}; + std::shared_ptr cachedStorageTexture(const Context& context, std::span words, const GuestTextureResource& resource, std::uint32_t mip, std::uint64_t guestBytes = 0, bool written = false); std::uint64_t heldBytes(const StorageTexture& image) { @@ -525,7 +554,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; @@ -601,8 +633,40 @@ std::array untrackedKey(std::uint64_t address, std::size_t byt return (key[2] & 1u) == 0 ? key : std::array{}; } +std::span firstLayerWords(std::span words, std::optional shape, std::array& narrowed, bool report) { + if (shape != ShaderRecompiler::DescriptorImageShape::Image2D || words.size() != narrowed.size() || IsNullTextureResource(words)) return words; + try { + if (!FirstLayerDescriptor(words, narrowed)) return words; + } catch (const std::runtime_error&) { + return words; + } + if (report) { + static std::atomic narrowings{0}; + const auto count = narrowings.fetch_add(1) + 1; + if (count <= 5 || (count & (count - 1)) == 0) { + const auto resource = DecodeTextureResource(words); + std::fprintf(stderr, "[gpu] 2D read of the 2D array 0x%llx (%ux%u format %u, slices %u..%u) built from slice %u alone; %llu such reads\n", static_cast(resource.baseAddress), resource.width, resource.height, resource.format, resource.baseArray, resource.depthOrLastArray, resource.baseArray, static_cast(count)); + } + } + return narrowed; +} + +void reportGuestAllocation(const GuestTextureResource& resource, std::uint64_t guestBytes) { + const auto range = GuestAllocations::GuestAllocationsLookup_nid_postfix(resource.baseAddress, false); + const auto geometry = DescribeSurface(resource); + if (range == nullptr) { + std::fprintf(stderr, "[gpu] view 0x%llx of %llu MiB lies in no registered guest allocation\n", static_cast(resource.baseAddress), static_cast(guestBytes >> 20u)); + return; + } + const auto end = range->address + range->bytes; + const auto fitting = geometry.thick || geometry.layerBytes == 0 ? 0ull : std::min(geometry.layers, (end - resource.baseAddress) / geometry.layerBytes); + std::fprintf(stderr, "[gpu] view 0x%llx of %llu MiB lies in the guest allocation 0x%llx..0x%llx (%llu MiB, range 0x%llx..0x%llx, %s%s%s): %llu of its %u slices fit in the range\n", static_cast(resource.baseAddress), static_cast(guestBytes >> 20u), static_cast(range->allocationAddress), static_cast(range->allocationAddress + range->allocationBytes), static_cast(range->allocationBytes >> 20u), static_cast(range->address), static_cast(end), range->readable ? "r" : "-", range->writable ? "w" : "-", range->gpu ? "g" : "-", static_cast(fitting), geometry.layers); +} + // `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)) { @@ -633,6 +697,21 @@ std::shared_ptr cachedTexture(const Context& context, std::span windows{0}; + const auto count = windows.fetch_add(1) + 1; + if (count <= 5 || (count & (count - 1)) == 0) std::fprintf(stderr, "[gpu] sampled array view 0x%llx (%ux%u format %u, dimension %d, slices %u..%u) served from its viewed slices at 0x%llx; %llu such views\n", static_cast(resource.baseAddress), resource.width, resource.height, resource.format, static_cast(resource.dimension), resource.baseArray, resource.depthOrLastArray, static_cast(window.baseAddress), static_cast(count)); + return cachedTexture(context, words, window, components, 0, depthCompare); + } + if (guestBytes == 0) guestBytes = DescribeSurface(resource).guestBytes; + if (guestBytes >= (1ull << 30u)) { + static std::atomic large{0}; + const auto count = large.fetch_add(1) + 1; + if (count <= 5 || (count & (count - 1)) == 0) { + std::fprintf(stderr, "[gpu] sampled view 0x%llx (%ux%u format %u, dimension %d, tile %d, slices %u..%u) covers %llu MiB; %llu such views\n", static_cast(resource.baseAddress), resource.width, resource.height, resource.format, static_cast(resource.dimension), static_cast(resource.tileMode), resource.baseArray, resource.depthOrLastArray, static_cast(guestBytes >> 20u), static_cast(count)); + reportGuestAllocation(resource, guestBytes); + } + } static const bool disabled = std::getenv("APS5_NO_TEXTURE_CACHE") != nullptr; const bool profile = LookupOutcomes::Profiled(); const auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; @@ -642,10 +721,10 @@ std::shared_ptr cachedTexture(const Context& context, std::span(guestBytes); + static_cast(FlushDepthPlanes(address, bytes)); // A storage image whose results for this surface (or for a mip chain containing it) are still on // the GPU supplies the texture by a view of it; anything else needs those results in guest // memory first. @@ -787,18 +866,19 @@ std::shared_ptr cachedTexture(const Context& context, std::spantexture; } - } else if (source == nullptr && it->bytes.size() == guestBytes && it->keys == *keys) { + } else if (source == nullptr && it->surfaceBytes == guestBytes && it->keys == *keys) { // Unwritten pages need no comparison; partially resident textures compare only committed // pages. The compare goes through the flush hook: it waits for recorded work over the // surface (counted, and named for the [hooksync] line). const auto equalsCommitted = [&] { + if (it->bytes.empty()) return false; if (Recorder::SnapshotWriteOverlaps(address, bytes)) counters.pendingReads.fetch_add(1, std::memory_order_relaxed); const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::TextureCompare); return GuestMemory::EqualsCommitted(address, it->bytes); }; const auto unchanged = [&] { if (it->committed != committed) return false; - return committed.has_value() ? !Recorder::SnapshotWriteOverlaps(address, it->bytes.size()) && GuestMemory::UnchangedCommittedSince(*committed, it->generation) : GuestMemory::UnchangedSince(address, it->bytes.size(), it->generation); + return committed.has_value() ? !Recorder::SnapshotWriteOverlaps(address, static_cast(it->surfaceBytes)) && GuestMemory::UnchangedCommittedSince(*committed, it->generation) : GuestMemory::UnchangedSince(address, static_cast(it->surfaceBytes), it->generation); }; const auto kept = [&] { if (untracked[0] == 0) return false; @@ -822,6 +902,7 @@ std::shared_ptr cachedTexture(const Context& context, std::span(source != nullptr ? 0u : bytes), nullptr, *keys, snapshotGeneration}; entry.committed = std::move(committed); + entry.surfaceBytes = bytes; if (source != nullptr) { entry.source = source; entry.sourceVersion = source->Version(); @@ -856,6 +937,8 @@ std::shared_ptr cachedTexture(const Context& context, std::span(context, *context.detiler, resource, components, entry.bytes, depthCompare); entry.accounted = entry.texture->AllocationBytes(); + constexpr std::size_t SnapshotCompareBytes = 256u << 10u; + if (entry.bytes.size() > SnapshotCompareBytes && (entry.generation != 0 || entry.untracked[0] != 0)) std::vector().swap(entry.bytes); counters.snapshots.fetch_add(1, std::memory_order_relaxed); } const auto budget = sampledBudget(context, cache); @@ -1025,11 +1108,23 @@ 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; - if (written && DepthSurfaceOverlaps(context.device, viewed.baseAddress, guestBytes != 0 ? guestBytes : DescribeSurface(viewed).guestBytes)) { + const auto viewedBytes = guestBytes != 0 ? guestBytes : DescribeSurface(viewed).guestBytes; + if (viewedBytes >= (1ull << 30u)) { + static std::atomic large{0}; + const auto count = large.fetch_add(1) + 1; + if (count <= 5 || (count & (count - 1)) == 0) { + std::fprintf(stderr, "[gpu] storage view 0x%llx (%ux%u format %u, dimension %d, tile %d, slices %u..%u, mip %u) covers %llu MiB; %llu such views\n", static_cast(viewed.baseAddress), viewed.width, viewed.height, viewed.format, static_cast(viewed.dimension), static_cast(viewed.tileMode), viewed.baseArray, viewed.depthOrLastArray, mip, static_cast(viewedBytes >> 20u), static_cast(count)); + reportGuestAllocation(viewed, viewedBytes); + } + } + if (DepthPlanesStale() || written) static_cast(FlushDepthPlanes(viewed.baseAddress, static_cast(viewedBytes))); + if (written && DepthSurfaceOverlaps(context.device, viewed.baseAddress, viewedBytes)) { const auto storage = WrittenStorageBytes(viewed, mip); RetireCoveredDepthSurfaces(context.device, storage.bytes, storage.extents); } @@ -1114,7 +1209,7 @@ std::uint64_t StorageTextureBudget(const VkPhysicalDeviceMemoryProperties& memor if (reported == nullptr || !heap || reported->heapBudget[*heap] == 0) return TextureCacheBudget(memory); const auto budget = reported->heapBudget[*heap]; const auto usage = reported->heapUsage[*heap]; - const auto reserved = usage - std::min(usage, sampledBytes + storageBytes) + std::max(2048ull << 20u, sampledBytes) + budget / 8u; + const auto reserved = usage - std::min(usage, sampledBytes + storageBytes) + sampledBytes + budget / 8u; const auto cap = std::min(TextureCacheBudget(memory), budget / 4u); return std::max(512ull << 20u, std::min(cap, budget > reserved ? budget - reserved : 0u)); } @@ -1141,6 +1236,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; { @@ -1793,6 +1892,11 @@ void ShaderResources::buildPrepare(std::span shaders, cons for (const auto& shader : shaders) { Require(shader.program != nullptr, "missing compiled shader"); ShaderRecompiler::RuntimeAbi::RequireVersion(shader.program->runtimeAbiVersion); + if (shader.program->poisonedSrtReads != 0) { + const auto& programBindings = shader.program->bindings; + const auto flat = std::find_if(programBindings.begin(), programBindings.end(), [](const ShaderRecompiler::DescriptorBinding& binding) { return binding.role == ShaderRecompiler::DescriptorRole::FlattenedSrt; }); + if (flat != programBindings.end()) srtGuardFaults.push_back({IrStage(shader.stage), flat->guestDescriptor}); + } const VkShaderStageFlags flags = VulkanStage(shader.stage); std::uint64_t stageStorageBuffers = 0; std::uint64_t stageResources = shader.stage == ShaderRecompiler::ShaderStage::Fragment ? colorAttachments : 0; @@ -1869,6 +1973,8 @@ void ShaderResources::buildPrepare(std::span shaders, cons const bool read = element < binding.bufferRead.size() && binding.bufferRead[element]; const std::uint32_t extent = element < binding.bufferExtent.size() ? binding.bufferExtent[element] : 0u; const auto index = addGuestBuffer(std::span(binding.guestDescriptor).subspan(static_cast(element) * 4, 4), target, indexAddress, indexBytes, written, atomic, read, extent); + const bool classified = element < binding.bufferWritten.size() && element < binding.bufferRead.size() && element < binding.bufferAtomic.size(); + allocations[index].writeOnly = classified && allocations[index].guest && allocations[index].written && binding.bufferWritten[element] && !read && !atomic; const auto& push = shader.program->pushConstants; if (!push.empty()) { const auto position = shader.program->memoryOffsetDword * 4u + element; @@ -2064,6 +2170,14 @@ void ShaderResources::buildPrepare(std::span shaders, cons Check(context.Function("vkAllocateDescriptorSets")(context.device, &allocation, &_set), "vkAllocateDescriptorSets"); } } + writeOnlyRanges.clear(); + for (const auto& item : allocations) { + if (!item.writeOnly || item.size == 0) continue; + const auto begin = item.address; + const auto end = item.address + item.size; + const bool shared = std::any_of(allocations.begin(), allocations.end(), [&](const Allocation& other) { return !other.writeOnly && other.size != 0 && other.address < end && begin < other.address + other.size; }); + if (!shared) writeOnlyRanges.emplace_back(begin, end); + } timing.descriptorsMs = phase(BuildPhase::Descriptors); if (BuildProfiled()) { timing.prepareMs = std::chrono::duration(std::chrono::steady_clock::now() - stageStart).count(); @@ -2156,6 +2270,7 @@ DescribedSetLayouts DescribeSetLayouts(const Context& context, std::span(context, guestMemory); else if (usesFaultBuffer) bda = std::make_unique(context); + if (bda) bda->SetSrtGuards(std::move(srtGuardFaults)); finishImageTables(); phase(BuildPhase::Bda); if (_set != VK_NULL_HANDLE || fragment.set != VK_NULL_HANDLE) { @@ -2240,7 +2356,8 @@ void ShaderResources::buildComplete() { if (!recorded) continue; const auto& held = allocation.table; auto& counters = TextureCounts(); - if (held != nullptr && held->binding == binding.layout.binding && held->runs == runs && std::equal(held->images.begin(), held->images.end(), images.begin() + static_cast(firstImage), images.end(), [](const VkDescriptorImageInfo& a, const VkDescriptorImageInfo& b) { return a.sampler == b.sampler && a.imageView == b.imageView && a.imageLayout == b.imageLayout; })) { + const bool heldAlive = held != nullptr && held->textures != nullptr && std::none_of(held->textures->begin(), held->textures->end(), [](const std::weak_ptr& texture) { return texture.expired(); }); + if (heldAlive && held->binding == binding.layout.binding && held->runs == runs && std::equal(held->images.begin(), held->images.end(), images.begin() + static_cast(firstImage), images.end(), [](const VkDescriptorImageInfo& a, const VkDescriptorImageInfo& b) { return a.sampler == b.sampler && a.imageView == b.imageView && a.imageLayout == b.imageLayout; })) { keptWrites.emplace_back(firstWrite, writes.size()); counters.tableWritesKept.fetch_add(1, std::memory_order_relaxed); } else { @@ -2616,7 +2733,7 @@ namespace { // of the reused ones, fast = every image proved current from stamps, full = the lookups were repeated. // Why the fast path left an object to the full walk (the "fast-fail by reason" counts). using FastFail = ShaderResources::FastFail; -constexpr std::array(FastFail::Count)> FastFailNames{"no record", "collect", "pending image", "evicted image", "memory changed", "keys", "cleared view", "storage keys", "depth surface"}; +constexpr std::array(FastFail::Count)> FastFailNames{"no record", "collect", "pending image", "evicted image", "memory changed", "keys", "cleared view", "storage keys", "depth surface", "replaced texture", "audit"}; using OwnRefreshFallback = ShaderResources::OwnRefreshFallback; constexpr std::array(OwnRefreshFallback::Count)> OwnRefreshFallbackNames{"disabled", "snapshot texture", "cleared view", "foreign view", "surface key", "not imported", "uncached", "re-run failed"}; @@ -2744,7 +2861,7 @@ void countRevalidate(bool fast, bool ok, std::chrono::steady_clock::time_point s const auto reasons = byReason(profile.fastFails, FastFailNames); const auto fullReasons = byReason(profile.fullByReason, FastFailNames); const auto fallbacks = byReason(profile.ownFallbacks, OwnRefreshFallbackNames); - AgcDriver::ProfilePrint_nid_no_patch("[rescache] revalidate (10 s): %llu calls %.1f ms: fast %llu, full %llu, failed %llu; fast-fail by reason:%s; full walks: T1 refreshed %llu (%llu storage images, %llu view sources, %llu views served by their own pending source, %llu overlaps left by the refresh accepted), full by reason:%s, T1 fallback by reason:%s, proofs verified %llu; fast-fail pending image: own 0 (the query excepts the own object), foreign %llu, snapshot %llu, T1 eligible %llu; keys proven: %llu sampled, %llu storage; epoch gate: %llu registry scans skipped, %llu import loops skipped\n", static_cast(profile.calls.exchange(0, std::memory_order_relaxed)), profile.nanoseconds.exchange(0, std::memory_order_relaxed) / 1e6, static_cast(profile.fast.exchange(0, std::memory_order_relaxed)), static_cast(profile.full.exchange(0, std::memory_order_relaxed)), static_cast(profile.failed.exchange(0, std::memory_order_relaxed)), reasons.c_str(), static_cast(profile.ownRefreshed.exchange(0, std::memory_order_relaxed)), static_cast(profile.ownStorageRefreshes.exchange(0, std::memory_order_relaxed)), static_cast(profile.ownViewRefreshes.exchange(0, std::memory_order_relaxed)), static_cast(profile.ownSourceViews.exchange(0, std::memory_order_relaxed)), static_cast(profile.refreshedOverlaps.exchange(0, std::memory_order_relaxed)), fullReasons.c_str(), fallbacks.c_str(), static_cast(profile.proofsVerified.exchange(0, std::memory_order_relaxed)), static_cast(profile.pendingForeign.exchange(0, std::memory_order_relaxed)), static_cast(profile.pendingSnapshot.exchange(0, std::memory_order_relaxed)), static_cast(profile.pendingT1Eligible.exchange(0, std::memory_order_relaxed)), static_cast(profile.keysProven.exchange(0, std::memory_order_relaxed)), static_cast(profile.storageKeysProven.exchange(0, std::memory_order_relaxed)), static_cast(profile.serialSkips.exchange(0, std::memory_order_relaxed)), static_cast(profile.importSkips.exchange(0, std::memory_order_relaxed))); + AgcDriver::ProfilePrint_nid_no_patch("[rescache] revalidate (10 s): %llu calls %.1f ms: fast %llu, full %llu, failed %llu; fast-fail by reason:%s; full walks: T1 refreshed %llu (%llu storage images, %llu view sources, %llu views served by their own pending source, %llu overlaps left by the refresh accepted), full by reason:%s, T1 fallback by reason:%s, proofs verified %llu; fast-fail pending image: own 0 (the query excepts the own object), foreign %llu, snapshot %llu, T1 eligible %llu; keys proven: %llu sampled, %llu storage; epoch gate: %llu registry scans skipped, %llu import loops skipped; lookup log: %llu builds and full walks kept their records past the bound\n", static_cast(profile.calls.exchange(0, std::memory_order_relaxed)), profile.nanoseconds.exchange(0, std::memory_order_relaxed) / 1e6, static_cast(profile.fast.exchange(0, std::memory_order_relaxed)), static_cast(profile.full.exchange(0, std::memory_order_relaxed)), static_cast(profile.failed.exchange(0, std::memory_order_relaxed)), reasons.c_str(), static_cast(profile.ownRefreshed.exchange(0, std::memory_order_relaxed)), static_cast(profile.ownStorageRefreshes.exchange(0, std::memory_order_relaxed)), static_cast(profile.ownViewRefreshes.exchange(0, std::memory_order_relaxed)), static_cast(profile.ownSourceViews.exchange(0, std::memory_order_relaxed)), static_cast(profile.refreshedOverlaps.exchange(0, std::memory_order_relaxed)), fullReasons.c_str(), fallbacks.c_str(), static_cast(profile.proofsVerified.exchange(0, std::memory_order_relaxed)), static_cast(profile.pendingForeign.exchange(0, std::memory_order_relaxed)), static_cast(profile.pendingSnapshot.exchange(0, std::memory_order_relaxed)), static_cast(profile.pendingT1Eligible.exchange(0, std::memory_order_relaxed)), static_cast(profile.keysProven.exchange(0, std::memory_order_relaxed)), static_cast(profile.storageKeysProven.exchange(0, std::memory_order_relaxed)), static_cast(profile.serialSkips.exchange(0, std::memory_order_relaxed)), static_cast(profile.importSkips.exchange(0, std::memory_order_relaxed)), static_cast(lookupBuildsPastBound.exchange(0, std::memory_order_relaxed))); } // APS5_NO_EPOCH_REVALIDATE=1: the per-element registry, cache and stamp checks and the per-region @@ -2754,6 +2871,19 @@ bool EpochRevalidate() { return enabled; } +std::chrono::steady_clock::duration TemplateAuditInterval() { + static const auto interval = [] { + const char* text = std::getenv("APS5_TEMPLATE_AUDIT_MS"); + return std::chrono::milliseconds(text != nullptr ? std::strtoull(text, nullptr, 10) : 1000ull); + }(); + return interval; +} + +bool TemplateAuditDue(std::chrono::steady_clock::time_point last) { + const auto interval = TemplateAuditInterval(); + return interval.count() != 0 && std::chrono::steady_clock::now() - last >= interval; +} + } // Turns the lookups' records into this object's per-texture validation records (see @@ -2773,6 +2903,10 @@ void ShaderResources::captureValidation() { lookupLog().clear(); } +std::size_t ShaderResources::LookupLogCapacity() { + return lookupLog().capacity(); +} + // Proves every texture and storage image still current without repeating its lookup: exactly the // "unchanged" branches of cachedTexture and StorageTexture::Refresh, evaluated from write stamps, // the pending-results registry and the DCC keys. Per element: collect first (stamps come only from @@ -2828,8 +2962,10 @@ bool ShaderResources::fastRevalidate(std::uint64_t serialBefore, std::spanReleased()) return fail(FastFail::Replaced); const auto address = surface.resource.baseAddress; const auto bytes = static_cast(surface.bytes); + if (!FlushDepthPlanes(address, bytes).empty()) return fail(FastFail::Changed); surface.collected = GuestMemory::CollectWrites(address, bytes); if (surface.collected == 0) return fail(FastFail::Collect); const auto* source = surface.source; @@ -2954,8 +3090,10 @@ bool ShaderResources::fastRevalidateEach() { if (!DepthSurfaceHolds(context, surface.resource, textures[i].get())) return false; continue; } + if (textures[i]->Released()) return false; const auto address = surface.resource.baseAddress; const auto bytes = static_cast(surface.bytes); + if (!FlushDepthPlanes(address, bytes).empty()) return false; surface.collected = GuestMemory::CollectWrites(address, bytes); if (surface.collected == 0) return false; if (PendingStorageOverlaps(address, bytes, surface.source)) return false; @@ -3129,8 +3267,12 @@ bool ShaderResources::Revalidate(std::span shaders, ProofR if (!reusable || shaders.empty()) return false; static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; const auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; - for (const auto& region : directRegions) CompletePendingCpuWrites(context, region.begin, static_cast(region.end - region.begin)); + for (const auto& region : directRegions) { + static_cast(FlushDepthPlanes(region.begin, static_cast(region.end - region.begin))); + CompletePendingCpuWrites(context, region.begin, static_cast(region.end - region.begin)); + } for (const auto& range : describedRanges) { + if (!range.imageTable) RequireNoMultisampleColorTarget(range.address, static_cast(range.bytes), "a cached image access"); CompletePendingCpuWrites(context, range.address, static_cast(range.bytes)); if (range.dccAddress != 0) CompletePendingCpuWrites(context, range.dccAddress, DccKeyBytes(range.bytes)); } @@ -3153,7 +3295,9 @@ bool ShaderResources::Revalidate(std::span shaders, ProofR // they must hand back the very objects the set's views belong to. The build appended them stage // by stage, binding by binding, so the walk repeats that order. const auto fullWalk = [&] { + lastFullWalk = std::chrono::steady_clock::now(); lookupLog().clear(); + BuildLookups lookups; std::size_t textureIndex = 0; std::size_t storageIndex = 0; for (const auto& shader : shaders) { @@ -3163,7 +3307,9 @@ bool ShaderResources::Revalidate(std::span shaders, ProofR if (binding.kind == ShaderRecompiler::DescriptorKind::SampledImage) { const auto elementWords = binding.count != 0 ? binding.guestDescriptor.size() / binding.count : 0; for (std::uint32_t element = 0; element < binding.count; ++element) { - const auto words = std::span(binding.guestDescriptor).subspan(static_cast(element) * elementWords, elementWords); + const auto raw = std::span(binding.guestDescriptor).subspan(static_cast(element) * elementWords, elementWords); + std::array narrowed{}; + const auto words = firstLayerWords(raw, binding.imageShape, narrowed, false); if (IsNullTextureResource(words)) continue; const auto resource = DecodeTextureResource(words); const VkComponentMapping components = ViewComponents(resource); @@ -3211,7 +3357,9 @@ bool ShaderResources::Revalidate(std::span shaders, ProofR } for (std::uint32_t element = 0; element < images->count; ++element) { if (table.elementTextures[element] == std::numeric_limits::max()) continue; - const auto words = std::span(images->guestDescriptor).subspan(static_cast(element) * 8u, 8u); + const auto raw = std::span(images->guestDescriptor).subspan(static_cast(element) * 8u, 8u); + std::array narrowed{}; + const auto words = firstLayerWords(raw, images->imageShapes.at(element), narrowed, false); const auto resource = DecodeTextureResource(words); const VkImageViewType multisampleViewType = images->imageShapes.at(element) == ShaderRecompiler::DescriptorImageShape::Image2DMultisampleArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D; if (textureIndex >= textures.size() || table.elementTextures[element] != textureIndex || cachedTexture(context, words, resource, ViewComponents(resource), 0, images->imageDepthCompare.at(element), multisampleViewType) != textures[textureIndex]) return false; @@ -3253,6 +3401,11 @@ bool ShaderResources::Revalidate(std::span shaders, ProofR } if (fallback != OwnRefreshFallback::Count) countOwnRefreshFallback(fallback); } + if (fast && TemplateAuditDue(lastFullWalk)) { + fast = false; + ownRefreshed = false; + reason = FastFail::Audit; + } if (report != nullptr) report->path = !fast ? ProofPath::Full : ownRefreshed ? ProofPath::OwnRefreshed : ProofPath::Fast; if (!fast) { countFullWalk(reason); @@ -3260,7 +3413,7 @@ bool ShaderResources::Revalidate(std::span shaders, ProofR const auto failure = [&] { switch (reason) { case FastFail::Pending: return ProofFailure::Pending; - case FastFail::Evicted: return ProofFailure::Evicted; + case FastFail::Evicted: case FastFail::Replaced: return ProofFailure::Evicted; case FastFail::Changed: return ProofFailure::Changed; case FastFail::Keys: case FastFail::ClearedView: case FastFail::StorageKeys: return ProofFailure::Keys; default: return ProofFailure::Other; @@ -4025,7 +4178,8 @@ std::size_t ShaderResources::addGuestBuffer(std::span words } Require(byteSize <= context.limits.maxStorageBufferRange, "shader buffer exceeds descriptor range limit"); Require(byteSize <= std::numeric_limits::max(), "shader buffer size exceeds host address space"); - const auto size = static_cast(byteSize); + auto size = static_cast(byteSize); + if (const std::uint64_t stride = descriptor.Stride(); !written && target != nullptr && stride != 0 && address < target->address && address + size > target->address && address + size - target->address < stride) size = static_cast(target->address - address); Require(target == nullptr || !overlap(address, size, target->address, target->bytes), "shader buffer aliases the render target"); // APS5_ALL_BUFFERS_WRITTEN=1: every element is noted as written, as before bufferWritten existed. static const bool allWritten = std::getenv("APS5_ALL_BUFFERS_WRITTEN") != nullptr; @@ -4110,7 +4264,7 @@ std::size_t ShaderResources::addDataBuffer(std::span words) const auto size = words.size() * sizeof(std::uint32_t); Require(size <= context.limits.maxStorageBufferRange, "shader data buffer exceeds descriptor range limit"); const bool refreshable = TemplateDataRefresh() && size <= MaxRefreshBytes; - auto buffer = std::make_unique(context, size, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | (refreshable ? VK_BUFFER_USAGE_TRANSFER_DST_BIT : 0u)); + auto buffer = std::make_unique(context, size, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); std::memcpy(buffer->Bytes().data(), words.data(), size); Allocation allocation{0, size, false, std::move(buffer)}; if (refreshable) allocation.dataWords.assign(words.begin(), words.end()); @@ -4186,6 +4340,31 @@ bool ShaderResources::refreshData(VkCommandBuffer commands, const CompiledShader return recorded; } +std::optional ShaderResources::shaderDataAllocation(const CompiledShader& shader) const { + Require(shader.program != nullptr, "missing compiled shader"); + const auto& program = *shader.program; + if (program.bindings.size() != bindings.size()) return std::nullopt; + for (std::size_t index = 0; index < program.bindings.size(); ++index) { + if (program.bindings[index].role != ShaderRecompiler::DescriptorRole::ShaderData || bindings[index].allocations.size() != 1) continue; + const auto allocation = bindings[index].allocations.front(); + if (allocations[allocation].guest || allocations[allocation].buffer == nullptr) return std::nullopt; + return allocation; + } + return std::nullopt; +} + +VkBuffer ShaderResources::shaderDataBuffer(const CompiledShader& shader) const { + const auto allocation = shaderDataAllocation(shader); + return allocation.has_value() ? allocations[*allocation].buffer->Handle() : VK_NULL_HANDLE; +} + +void ShaderResources::forgetShaderDataWords(const CompiledShader& shader) { + if (const auto allocation = shaderDataAllocation(shader)) { + allocations[*allocation].dataWords.clear(); + rehashDataWords(); + } +} + void ShaderResources::writeDataWords(VkCommandBuffer commands, std::size_t allocation, std::span words) const { const auto& buffer = *allocations[allocation].buffer; const auto size = words.size() * sizeof(std::uint32_t); @@ -4317,8 +4496,10 @@ bool ShaderResources::precollectImages() { const auto& binding = *deferred.binding; const auto elementWords = binding.guestDescriptor.size() / binding.count; for (std::uint32_t element = 0; element < binding.count; ++element) { - const auto words = std::span(binding.guestDescriptor).subspan(static_cast(element) * elementWords, elementWords); const bool sampled = binding.kind == ShaderRecompiler::DescriptorKind::SampledImage; + const auto raw = std::span(binding.guestDescriptor).subspan(static_cast(element) * elementWords, elementWords); + std::array narrowed{}; + const auto words = firstLayerWords(raw, sampled ? binding.imageShape : std::nullopt, narrowed, false); if (sampled && !imageRecords.empty()) { const auto& previous = imageRecords.back(); if (previous.sampled && previous.decoded && words.size() == 8 && std::equal(words.begin(), words.end(), previous.words.begin()) && (binding.imageDepthCompare.empty() || !binding.imageDepthCompare.at(element))) { @@ -4382,7 +4563,8 @@ std::shared_ptr ShaderResources::fastTexture(const ImageRecord& record) // between the stages marks them uncompressed). const auto address = record.resource.baseAddress; const auto bytes = static_cast(record.guestBytes); - if (DepthSurfaceAt(address)) return nullptr; + RequireNoMultisampleColorTarget(address, bytes != 0 ? bytes : 1u, "a texture read"); + if (DepthSurfaceAt(address) || !FlushDepthPlanes(address, bytes).empty()) return nullptr; if (GuestMemory::CollectWrites(address, bytes) == 0) return nullptr; if (PendingStorageOverlaps(address, bytes, record.source.get())) return nullptr; auto keys = record.keys; @@ -4442,7 +4624,9 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind Require(binding.imageSamplers.size() == binding.count, "guest sampled image binding is missing its image-sampler pairs"); const auto elementWords = binding.guestDescriptor.size() / binding.count; for (std::uint32_t element = 0; element < binding.count; ++element) { - const auto words = std::span(binding.guestDescriptor).subspan(static_cast(element) * elementWords, elementWords); + const auto raw = std::span(binding.guestDescriptor).subspan(static_cast(element) * elementWords, elementWords); + std::array narrowed{}; + const auto words = firstLayerWords(raw, binding.imageShape, narrowed, true); const auto* record = nextRecord(); if (IsNullTextureResource(words)) { item.imageAllocations.push_back(std::numeric_limits::max()); @@ -4452,14 +4636,15 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind const auto resource = record != nullptr && record->decoded ? record->resource : DecodeTextureResource(words); const bool forcedSrgb = ForcesDegammaOn(resource.format, binding.imageSamplers[element], shaderSamplers); RequireNonSeamlessCube(resource.dimension == TextureDimension::kCube, binding.imageSamplers[element], shaderSamplers); - const bool firstLayer = binding.imageShape == ShaderRecompiler::DescriptorImageShape::Image2D && resource.dimension == TextureDimension::k2DArray; - if (!firstLayer && !MatchesGuestDimension(*binding.imageShape, resource.dimension)) throw std::runtime_error("AGC graphics: guest texture dimension disagrees with the shader's declared image shape (shape " + std::to_string(static_cast(*binding.imageShape)) + ", dimension " + std::to_string(static_cast(resource.dimension)) + ")"); + const bool depthCompare = !binding.imageDepthCompare.empty() && binding.imageDepthCompare.at(element); + const bool firstLayer = binding.imageShape == ShaderRecompiler::DescriptorImageShape::Image2D && (resource.dimension == TextureDimension::k2DArray || (depthCompare && resource.dimension == TextureDimension::k1DArray)); + const bool depth1D = depthCompare && ((binding.imageShape == ShaderRecompiler::DescriptorImageShape::Image2D && resource.dimension == TextureDimension::k1D) || (binding.imageShape == ShaderRecompiler::DescriptorImageShape::Image2DArray && resource.dimension == TextureDimension::k1DArray)); + if (!firstLayer && !depth1D && !MatchesGuestDimension(*binding.imageShape, resource.dimension)) throw std::runtime_error("AGC graphics: guest texture dimension disagrees with the shader's declared image shape (shape " + std::to_string(static_cast(*binding.imageShape)) + ", dimension " + std::to_string(static_cast(resource.dimension)) + ")"); const VkComponentMapping components = ViewComponents(resource); const auto guestBytes = record != nullptr && record->decoded ? record->guestBytes : DescribeSurface(resource).guestBytes; CompletePendingCpuWrites(context, resource.baseAddress, static_cast(guestBytes)); if (resource.dccAddress != 0) CompletePendingCpuWrites(context, resource.dccAddress, DccKeyBytes(guestBytes)); std::shared_ptr texture; - const bool depthCompare = !binding.imageDepthCompare.empty() && binding.imageDepthCompare.at(element); const VkImageViewType multisampleViewType = binding.imageShape == ShaderRecompiler::DescriptorImageShape::Image2DMultisampleArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D; if (previousSampled.texture != nullptr && previousSampled.depthCompare == depthCompare && previousSampled.multisampleViewType == multisampleViewType && words.size() == 8 && std::equal(words.begin(), words.end(), previousSampled.words.begin())) texture = previousSampled.texture; if (texture == nullptr && record != nullptr && record->texture != nullptr) { @@ -4718,7 +4903,7 @@ void ShaderResources::resolveImageTables() { const auto place = [&](std::uint32_t element, std::shared_ptr texture, const GuestTextureResource& resource, std::uint64_t guestBytes, bool firstLayer) { textures.push_back(std::move(texture)); textureFirstLayer.push_back(firstLayer); - describedRanges.push_back({"texture", resource.baseAddress, guestBytes, resource.width, resource.height, resource.format, static_cast(resource.tileMode), resource.dccAddress}); + describedRanges.push_back({"texture", resource.baseAddress, guestBytes, resource.width, resource.height, resource.format, static_cast(resource.tileMode), resource.dccAddress, true}); table.elementTextures[element] = textures.size() - 1u; item->imageAllocations[static_cast(table.elementBase) + element] = textures.size() - 1u; }; @@ -4797,8 +4982,10 @@ void ShaderResources::resolveImageTables() { try { resource = DecodeTextureResource(words); shape = binding.imageShapes.at(element); - firstLayer = shape == ShaderRecompiler::DescriptorImageShape::Image2D && resource.dimension == TextureDimension::k2DArray; - if (!firstLayer && !MatchesGuestDimension(shape, resource.dimension)) throw std::runtime_error("AGC graphics: guest texture dimension disagrees with the table entry's image shape (shape " + std::to_string(static_cast(shape)) + ", dimension " + std::to_string(static_cast(resource.dimension)) + ")"); + const bool depthCompare = element < binding.imageDepthCompare.size() && binding.imageDepthCompare[element]; + firstLayer = shape == ShaderRecompiler::DescriptorImageShape::Image2D && (resource.dimension == TextureDimension::k2DArray || (depthCompare && resource.dimension == TextureDimension::k1DArray)); + const bool depth1D = depthCompare && ((shape == ShaderRecompiler::DescriptorImageShape::Image2D && resource.dimension == TextureDimension::k1D) || (shape == ShaderRecompiler::DescriptorImageShape::Image2DArray && resource.dimension == TextureDimension::k1DArray)); + if (!firstLayer && !depth1D && !MatchesGuestDimension(shape, resource.dimension)) throw std::runtime_error("AGC graphics: guest texture dimension disagrees with the table entry's image shape (shape " + std::to_string(static_cast(shape)) + ", dimension " + std::to_string(static_cast(resource.dimension)) + ")"); guestBytes = DescribeSurface(resource).guestBytes; if (!GuestMemory::Accessible(reinterpret_cast(resource.baseAddress), static_cast(guestBytes)) && GuestMemory::CommittedRanges(resource.baseAddress, static_cast(guestBytes), false).empty()) throw std::runtime_error("AGC graphics: the table entry's surface at 0x" + HexText(resource.baseAddress) + " (0x" + HexText(guestBytes) + " bytes) is not mapped"); static_cast(ResolveTextureFormat(resource.format)); @@ -4837,7 +5024,7 @@ void ShaderResources::resolveImageTables() { storageWritten.push_back(true); storageAtomic.push_back(false); storageAtomic64.push_back(false); - describedRanges.push_back({"storage", resource.baseAddress, guestBytes, resource.width, resource.height, resource.format, static_cast(resource.tileMode), resource.dccAddress}); + describedRanges.push_back({"storage", resource.baseAddress, guestBytes, resource.width, resource.height, resource.format, static_cast(resource.tileMode), resource.dccAddress, true}); bindings[storageBinding].imageAllocations[absolute] = storageTextures.size() - 1u; continue; } @@ -4846,6 +5033,12 @@ void ShaderResources::resolveImageTables() { reusable = false; continue; } + std::array narrowed{}; + const auto sampledWords = firstLayerWords(words, binding.imageShapes.at(element), narrowed, true); + if (sampledWords.data() != words.data()) { + resource = DecodeTextureResource(sampledWords); + guestBytes = DescribeSurface(resource).guestBytes; + } CompletePendingCpuWrites(context, resource.baseAddress, static_cast(guestBytes)); if (resource.dccAddress != 0) CompletePendingCpuWrites(context, resource.dccAddress, DccKeyBytes(guestBytes)); std::shared_ptr texture; @@ -4853,7 +5046,7 @@ void ShaderResources::resolveImageTables() { const bool depthCompare = binding.imageDepthCompare.at(element); const VkImageViewType multisampleViewType = shape == ShaderRecompiler::DescriptorImageShape::Image2DMultisampleArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D; try { - texture = cachedTexture(context, words, resource, components, guestBytes, depthCompare, multisampleViewType); + texture = cachedTexture(context, sampledWords, resource, components, guestBytes, depthCompare, multisampleViewType); } catch (const std::runtime_error& error) { table.map[faults + element] = rejectTableElement(table, words, error.what()); if (reusable) fresh.elements.push_back({{}, {}, {}, 0, false, true, error.what()}); @@ -4864,12 +5057,12 @@ void ShaderResources::resolveImageTables() { const auto* looked = log.empty() ? nullptr : &log.back(); reusable = looked != nullptr && looked->object == texture.get() && looked->source == nullptr && looked->generation != 0 && looked->keys == DccKeys::Uncompressed && resource.dccAddress == 0; if (reusable) { - fresh.elements.push_back({MakeTextureKey(context.device, words, components, depthCompare), texture, resource, guestBytes, firstLayer, false, {}}); + fresh.elements.push_back({MakeTextureKey(context.device, sampledWords, components, depthCompare), texture, resource, guestBytes, firstLayer, false, {}}); fresh.queries.push_back({resource.baseAddress, static_cast(guestBytes), looked->generation}); fresh.ranges.emplace_back(resource.baseAddress, resource.baseAddress + guestBytes); } } - fresh.bound[element] = bind(element, words, std::move(texture), resource, guestBytes, firstLayer); + fresh.bound[element] = bind(element, sampledWords, std::move(texture), resource, guestBytes, firstLayer); } counters.tablesResolved.fetch_add(1, std::memory_order_relaxed); if (reusable && CurrentTableState() == state) { @@ -5012,11 +5205,18 @@ ShaderResources::~ShaderResources() { release(); } -std::shared_ptr ShaderResources::keptTable(std::size_t set, std::shared_ptr>>& held) noexcept { +std::shared_ptr ShaderResources::keptTable(std::size_t set, std::shared_ptr>>& held) noexcept { const auto& table = tableWrites[set]; if (!table.written || KeptSets() == 0u || table.firstImage + table.images > descriptorImages.size()) return nullptr; try { - if (held == nullptr) held = std::make_shared>>(std::move(textures)); + if (held == nullptr) { + std::vector> weak; + weak.reserve(textures.size()); + for (const auto& texture : textures) { + if (texture != nullptr) weak.push_back(texture); + } + held = std::make_shared>>(std::move(weak)); + } auto kept = std::make_shared(); kept->binding = table.binding; kept->runs = table.runs; @@ -5030,7 +5230,7 @@ std::shared_ptr ShaderResources::keptTable(std::size_t s void ShaderResources::release() noexcept { // A set from the cache's pool chain goes back to it; a dedicated pool dies with its set. - std::shared_ptr>> held; + std::shared_ptr>> held; if (cacheAllocation.pool && context.descriptorCache != nullptr) context.descriptorCache->Free(cacheAllocation, keptTable(0, held)); if (pool) context.Function("vkDestroyDescriptorPool")(context.device, pool, nullptr); if (_layout && ownsLayout) context.Function("vkDestroyDescriptorSetLayout")(context.device, _layout, nullptr); @@ -5370,13 +5570,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) { @@ -5422,9 +5629,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 61438856a5..40f39f6a74 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp @@ -51,6 +51,8 @@ struct Module { std::uint32_t primitiveIndices = 0; bool fragmentBarycentric = false; bool layerOutput = false; + bool meshCapability = false; + std::uint32_t primitiveInputs = 0; std::map> modes; const std::vector& Type(std::uint32_t id) const { @@ -75,17 +77,17 @@ struct Module { throw std::runtime_error("AGC graphics: unsupported SPIR-V interface type"); } - std::uint32_t AddLocations(std::map& locations, std::uint32_t location, std::uint32_t typeId, bool patch, std::uint32_t depth = 0) const { + std::uint32_t AddLocations(std::map& locations, std::uint32_t location, std::uint32_t typeId, bool patch, bool primitive = false, std::uint32_t depth = 0) const { Require(depth < 8 && location < 256, "graphics interface exceeds supported locations"); const auto& type = Type(typeId); if ((type[0] & 0xffffu) == spv::OpTypeArray) { Require(type.size() == 4, "malformed interface array"); const auto count = constants.find(type[3]); Require(count != constants.end() && count->second != 0 && count->second <= 256, "invalid interface array length"); - for (std::uint32_t i = 0; i < count->second; ++i) location = AddLocations(locations, location, type[2], patch, depth + 1); + for (std::uint32_t i = 0; i < count->second; ++i) location = AddLocations(locations, location, type[2], patch, primitive, depth + 1); return location; } - Require(locations.emplace(location | (patch ? 0x10000u : 0u), std::string(patch ? "patch:" : "vertex:") + Signature(typeId)).second, "duplicate shader interface location"); + Require(locations.emplace(location | (patch ? 0x10000u : 0u), std::string(patch ? "patch:" : primitive ? "primitive:" : "vertex:") + Signature(typeId)).second, "duplicate shader interface location"); return location + 1; } @@ -154,7 +156,7 @@ struct Module { } }; -Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics, bool shaderClipDistance, bool shaderCullDistance, bool viewportIndexLayer, bool runtimeDescriptorArray, bool demoteToHelperInvocation, bool shaderTerminateInvocation) { +Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics, bool shaderClipDistance, bool shaderCullDistance, bool viewportIndexLayer, bool runtimeDescriptorArray, bool demoteToHelperInvocation, bool shaderTerminateInvocation, std::span primitiveLocations = {}) { using Stage = ShaderRecompiler::ShaderStage; Require(compiled.program != nullptr, "missing compiled shader"); const auto& shader = *compiled.program; @@ -238,8 +240,9 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi (control || evaluation) && capability == spv::CapabilityTessellation; + if (fragment && capability == spv::CapabilityMeshShadingEXT) module.meshCapability = true; const bool isMeshCapability = - mesh && + (mesh || (fragment && !primitiveLocations.empty())) && capability == spv::CapabilityMeshShadingEXT; const bool isGeometryCapability = @@ -342,7 +345,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi terminateExtension = true; break; } - Require(extension == "SPV_KHR_float_controls" || (imageInt64Atomics && extension == "SPV_EXT_shader_image_int64") || (viewportIndexLayer && extension == "SPV_EXT_shader_viewport_index_layer") || (fragment && demoteToHelperInvocation && extension == "SPV_EXT_demote_to_helper_invocation") || (mesh && (extension == "SPV_EXT_mesh_shader" || extension == "SPV_KHR_physical_storage_buffer")) || ((descriptorIndexing || runtimeDescriptorArray) && extension == "SPV_EXT_descriptor_indexing") || (shader.bdaAbiVersion == ShaderRecompiler::BdaAbi::Version && (extension == "SPV_KHR_physical_storage_buffer" || extension == "SPV_KHR_8bit_storage")), "unsupported SPIR-V extension"); + Require(extension == "SPV_KHR_float_controls" || (imageInt64Atomics && extension == "SPV_EXT_shader_image_int64") || (viewportIndexLayer && extension == "SPV_EXT_shader_viewport_index_layer") || (fragment && demoteToHelperInvocation && extension == "SPV_EXT_demote_to_helper_invocation") || (mesh && (extension == "SPV_EXT_mesh_shader" || extension == "SPV_KHR_physical_storage_buffer")) || (fragment && !primitiveLocations.empty() && extension == "SPV_EXT_mesh_shader") || ((descriptorIndexing || runtimeDescriptorArray) && extension == "SPV_EXT_descriptor_indexing") || (shader.bdaAbiVersion == ShaderRecompiler::BdaAbi::Version && (extension == "SPV_KHR_physical_storage_buffer" || extension == "SPV_KHR_8bit_storage")), "unsupported SPIR-V extension"); break; } case spv::OpTerminateInvocation: @@ -529,10 +532,13 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi module.secondSource = module.Signature(typeId); } else if (decoration.location) { Require(!vertexArray || (outer[0] & 0xffffu) == spv::OpTypeArray, "per-vertex interface lacks a control-point or mesh-output dimension"); - Require(!decoration.perPrimitive, "per-primitive user outputs are unsupported"); + Require(!decoration.perPrimitive || ((mesh && output) || (fragment && input)), "per-primitive user interface is unsupported for this stage"); + Require(!decoration.perPrimitive || !decoration.patch, "a per-primitive interface cannot be a patch"); + Require(!decoration.perPrimitive || std::ranges::find(primitiveLocations, *decoration.location) != primitiveLocations.end(), "per-primitive user interface at a location the barycentric emulation does not reserve"); Require(!decoration.builtin, "shader input cannot have both location and built-in decorations"); auto& locations = variable.storage == spv::StorageClassInput ? module.inputs : module.outputs; - module.AddLocations(locations, *decoration.location, typeId, decoration.patch); + module.AddLocations(locations, *decoration.location, typeId, decoration.patch, decoration.perPrimitive); + if (decoration.perPrimitive && fragment) module.primitiveInputs++; } else if (decoration.builtin) { module.Builtin(*decoration.builtin, typeId, variable.storage, stage, subgroup); } else { @@ -619,6 +625,8 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi Require(push == (!shader.pushConstants.empty() || shader.readsDepthRemapPush || mesh || (control && !rectListControl && !barycentricPatches)), "recompiler push constant metadata disagrees with SPIR-V"); for (const auto id : module.interface) Require(module.variables.contains(id), "entry point interface contains an unknown variable"); if (mesh) Require(module.primitiveIndices == state.stages.mesh->outputPrimitiveSize, "mesh shader does not export primitive indices"); + if (fragment) Require(module.primitiveInputs == 0 || !primitiveLocations.empty(), "per-primitive pixel inputs need the barycentric emulation layout"); + if (fragment) Require(!module.meshCapability || module.primitiveInputs != 0, "a pixel shader declares mesh shading without a per-primitive input"); if (stage == Stage::Vertex || mesh || evaluation || geometry) Require(module.position, "vertex shader does not export position"); return module; } @@ -657,13 +665,25 @@ std::set ValidateShaders(std::span shaders, } const std::array tessStages{Stage::Local, Stage::TessellationControl, Stage::TessellationEvaluation, Stage::Fragment}; static_cast(AssemblePushConstants(shaders)); + std::vector primitiveLocations; + if (mesh && !shaders.empty() && shaders.back().program != nullptr && shaders.back().program->barycentricEmulation.active) { + const auto& last = *shaders.back().program; + const auto layout = ShaderRecompiler::LayoutBarycentricEmulation(last.fragmentParameters, last.barycentricEmulation, true); + if (layout.positionsLocation != ShaderRecompiler::BarycentricEmulationLayout::NoLocation) { + for (std::uint32_t corner = 0; corner < 3u; ++corner) primitiveLocations.push_back(layout.positionsLocation + corner); + for (const auto& [guest, relocated] : layout.perVertexLocations) { + static_cast(guest); + for (std::uint32_t corner = 0; corner < 3u; ++corner) primitiveLocations.push_back(relocated + corner); + } + } + } Module previous; for (std::size_t i = 0; i < shaders.size(); ++i) { const auto expected = state.rectList || patches ? (i == 0 ? Stage::Vertex : tessStages[i]) : tessellation ? tessStages[i] : geometry && i == 1 ? Stage::Geometry : i == shaders.size() - 1 ? Stage::Fragment : state.stages.path == ShaderPath::Geometry ? Stage::Mesh : Stage::Vertex; Require(shaders[i].program != nullptr, "missing compiled shader"); Require(shaders[i].stage == expected, "graphics stage order disagrees"); for (const auto& binding : shaders[i].program->bindings) Require(binding.descriptorSet == ShaderRecompiler::RuntimeAbi::DescriptorSet || (shaders[i].stage == ShaderRecompiler::ShaderStage::Fragment && binding.descriptorSet == ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet), "graphics resource uses a descriptor set other than its stage's"); - const auto current = Inspect(shaders[i], state, subgroup, fragmentShaderBarycentric, descriptorIndexing, imageInt64Atomics, geometryShader, sampleRateShading, bufferInt64Atomics, shaderClipDistance, shaderCullDistance, viewportIndexLayer, runtimeDescriptorArray, demoteToHelperInvocation, shaderTerminateInvocation); + const auto current = Inspect(shaders[i], state, subgroup, fragmentShaderBarycentric, descriptorIndexing, imageInt64Atomics, geometryShader, sampleRateShading, bufferInt64Atomics, shaderClipDistance, shaderCullDistance, viewportIndexLayer, runtimeDescriptorArray, demoteToHelperInvocation, shaderTerminateInvocation, primitiveLocations); if (i != 0) { for (const auto& [location, signature] : current.inputs) { const auto output = previous.outputs.find(location); @@ -690,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 0dce8f8b2d..8ad7d14065 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -53,12 +53,48 @@ 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); return std::string(name) + " is unsupported" + detail; } +VkPolygonMode decodePolygonMode(const Registers& cx, std::uint32_t raster, const State& state, bool pointsOrLines) { + const auto mode = (raster >> 3u) & 3u; + if (mode == 0) return VK_POLYGON_MODE_FILL; + if (mode != 1) throw std::runtime_error("AGC graphics: " + zeroMessage(0x205, raster, "a reserved POLY_MODE")); + const auto front = (raster >> 5u) & 7u; + const auto back = (raster >> 8u) & 7u; + if (front > 2 || back > 2) throw std::runtime_error("AGC graphics: " + zeroMessage(0x205, raster, "a reserved polygon type")); + if (pointsOrLines && (state.stages.path == ShaderPath::Vertex || state.stages.mesh.has_value())) return VK_POLYGON_MODE_FILL; + const bool frontDrawn = (state.cullMode & VK_CULL_MODE_FRONT_BIT) == 0; + const bool backDrawn = (state.cullMode & VK_CULL_MODE_BACK_BIT) == 0; + if (!frontDrawn && !backDrawn) return VK_POLYGON_MODE_FILL; + if (frontDrawn && backDrawn && front != back) throw std::runtime_error("AGC graphics: " + zeroMessage(0x205, raster, "different front and back polygon modes")); + const auto type = frontDrawn ? front : back; + if (type == 2) return VK_POLYGON_MODE_FILL; + if (state.rectList) throw std::runtime_error("AGC graphics: " + zeroMessage(0x205, raster, "a point or line polygon mode on a rect list")); + if ((raster & 0x3800u) != 0) throw std::runtime_error("AGC graphics: " + zeroMessage(0x205, raster, "depth bias with a point or line polygon mode")); + if (type == 1) { + const auto line = find(cx, 0x282); + if (line == cx.end()) throw std::runtime_error("AGC graphics: a line polygon mode needs PA_SU_LINE_CNTL"); + if ((line->second & 0xffffu) != 8u) throw std::runtime_error("AGC graphics: " + zeroMessage(0x282, line->second, "a line polygon mode wider or narrower than one pixel")); + return VK_POLYGON_MODE_LINE; + } + const auto size = find(cx, 0x280); + const auto limits = find(cx, 0x281); + if (size == cx.end() || limits == cx.end()) throw std::runtime_error("AGC graphics: a point polygon mode needs PA_SU_POINT_SIZE and PA_SU_POINT_MINMAX"); + if (size->second != 0x80008u) throw std::runtime_error("AGC graphics: " + zeroMessage(0x280, size->second, "a point polygon mode with a point size other than one pixel")); + if ((limits->second & 0xffffu) > 8u || (limits->second >> 16u) < 8u) throw std::runtime_error("AGC graphics: " + zeroMessage(0x281, limits->second, "a point polygon mode whose size limits exclude one pixel")); + return VK_POLYGON_MODE_POINT; +} + std::string tessellationMessage(std::uint32_t config, std::uint32_t parameters, const ShaderRecompiler::TessellationConfiguration& tess) { static constexpr std::array Types{"TESS_ISOLINE", "TESS_TRIANGLE", "TESS_QUAD"}; static constexpr std::array Partitionings{"PART_INTEGER", "PART_POW2", "PART_FRAC_ODD", "PART_FRAC_EVEN"}; @@ -188,8 +224,9 @@ constexpr std::uint32_t PixelShaderIteratesSamples = 0x10000u; constexpr std::uint32_t PixelSampleCoverageInput = 0x4000u; constexpr std::uint32_t PixelSampleRateInputs = 0x2011u; constexpr std::uint32_t ScreenOffsetMask = ~0x01ff01ffu; -// Bits 26/27 (ZCLIP_NEAR/FAR_DISABLE) become depth clamping; bit 19 selects the [0, 1] clip space. -constexpr std::uint32_t ClipControlMask = ~(0x80000u | 0x01000000u | 0x0c000000u); +// Bits 26/27 (ZCLIP_NEAR/FAR_DISABLE) become depth clamping; bit 19 selects the [0, 1] clip space; +// bit 16 disables clipping. +constexpr std::uint32_t ClipControlMask = ~(0x10000u | 0x80000u | 0x01000000u | 0x0c000000u); bool zFormatSupported(std::uint32_t format) { return format == 0u || format == 1u || format == 2u || format == 3u || format == 9u; @@ -198,10 +235,11 @@ bool zFormatSupported(std::uint32_t format) { std::uint32_t shaderControlMask(std::uint32_t zFormat) { constexpr std::uint32_t zExportEnable = 0x1u; constexpr std::uint32_t stencilTestValExportEnable = 0x2u; + constexpr std::uint32_t stencilOpValExportEnable = 0x4u; constexpr std::uint32_t maskExportEnable = 0x100u; auto mask = ShaderControlMask; if (zFormat != 0u && zFormatSupported(zFormat)) mask &= ~zExportEnable; - if (zFormat == 2u || zFormat == 9u) mask &= ~stencilTestValExportEnable; + if (zFormat == 2u || zFormat == 9u) mask &= ~(stencilTestValExportEnable | stencilOpValExportEnable); if (zFormat == 9u) mask &= ~maskExportEnable; return mask; } @@ -234,7 +272,7 @@ bool depthPlanesAbsent(const Registers& cx) { return z != cx.end() && stencil != cx.end() && (z->second & 3u) == 0 && (stencil->second & 1u) == 0; } -VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint32_t refMask, bool readOnly, bool testValExport) { +VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint32_t refMask, bool readOnly, bool testValExport, bool opValExport, bool opValReference) { VkStencilOpState face{}; face.compareOp = static_cast(compare); face.compareMask = (refMask >> 8u) & 0xffu; @@ -245,7 +283,11 @@ VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint auto fixed = compare == 0 || compare == 7 ? 0u : face.compareMask; const auto convert = [&](std::uint32_t op) { if (face.writeMask == 0) return VK_STENCIL_OP_KEEP; - char detail[96]; + char detail[112]; + if (opValExport && ((op == 4 && !opValReference) || op == 5 || op == 6 || op == 8 || op == 9)) { + std::snprintf(detail, sizeof(detail), "AGC graphics: stencil operation %u with a pixel shader that exports the stencil operation value is unsupported", op); + throw std::runtime_error(detail); + } switch (op) { case 0: return VK_STENCIL_OP_KEEP; case 1: return VK_STENCIL_OP_ZERO; @@ -256,6 +298,10 @@ VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint if (op != 3) throw std::runtime_error("AGC graphics: stencil replacement by a value other than the test value is unsupported with a pixel shader that exports the stencil test value"); return VK_STENCIL_OP_REPLACE; } + if (opValReference) { + if (op != 4) throw std::runtime_error("AGC graphics: stencil replacement by a value other than the operation value is unsupported with a pixel shader that exports the stencil operation value"); + return VK_STENCIL_OP_REPLACE; + } const auto value = op == 2 ? 0xffu : op == 3 ? test : opValue; if (((reference ^ value) & fixed & face.writeMask) != 0) { std::snprintf(detail, sizeof(detail), "AGC graphics: stencil replacement 0x%02x against test value 0x%02x (masks 0x%02x/0x%02x) is unsupported", value, test, face.compareMask, face.writeMask); @@ -284,10 +330,12 @@ VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint face.passOp = convert((ops >> 4u) & 0xfu); face.depthFailOp = convert((ops >> 8u) & 0xfu); face.reference = reference; + Require(!opValReference || compare == 0 || compare == 7 || face.compareMask == 0, "a stencil compare against the test value is unsupported with a pixel shader that exports the stencil operation value"); return face; } -void decodeDepth(const Registers& cx, std::uint32_t depthControl, bool testValExport, State& result) { +void decodeDepth(const Registers& cx, std::uint32_t depthControl, bool testValExport, bool opValExport, State& result) { + const bool opValReference = opValExport && StencilOperationValueReplaces(cx); zero(cx, 0x000, 0x0000101cu, "depth copy, resummarize or decompress draws (DB_RENDER_CONTROL)"); const bool depthClear = (read(cx, 0x000) & 1u) != 0; const bool stencilClear = (read(cx, 0x000) & 2u) != 0; @@ -350,8 +398,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; @@ -374,8 +422,8 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, bool testValEx result.stencilBack = clear; } else if (result.stencilTest) { const auto ops = read(cx, 0x10b); - result.stencilFront = stencilFace((depthControl >> 8u) & 7u, ops, read(cx, 0x10c), stencilReadOnly, testValExport); - result.stencilBack = (depthControl & 0x80u) != 0 ? stencilFace((depthControl >> 20u) & 7u, ops >> 12u, read(cx, 0x10d), stencilReadOnly, testValExport) : result.stencilFront; + result.stencilFront = stencilFace((depthControl >> 8u) & 7u, ops, read(cx, 0x10c), stencilReadOnly, testValExport, opValExport, opValReference); + result.stencilBack = (depthControl & 0x80u) != 0 ? stencilFace((depthControl >> 20u) & 7u, ops >> 12u, read(cx, 0x10d), stencilReadOnly, testValExport, opValExport, opValReference) : result.stencilFront; } } @@ -908,6 +956,53 @@ VkExtent2D SampleLocationGrid(const State& state) { return state.sampleLocationGrid ? VkExtent2D{2, 2} : VkExtent2D{1, 1}; } +std::optional SameSetSampleOrder(const State& state) { + if (!state.customSampleLocations || !state.sampleLocationGrid || state.samples < 2u || state.samples > 8u) return std::nullopt; + SampleOrder order{}; + for (std::uint32_t pixel = 0; pixel < order.size(); ++pixel) { + std::uint32_t word = KeptSampleOrder; + std::uint32_t taken = 0; + for (std::uint32_t sample = 0; sample < state.samples; ++sample) { + std::uint32_t guest = 0; + while (guest < state.samples && (((taken >> guest) & 1u) != 0u || state.sampleLocations[pixel * 8u + guest] != state.sampleLocations[sample])) ++guest; + if (guest == state.samples) return std::nullopt; + taken |= 1u << guest; + word = (word & ~(0xfu << (sample * 4u))) | (guest << (sample * 4u)); + } + order[pixel] = word; + } + return order; +} + +bool ReordersSamples(const SampleOrder& order) { + return std::ranges::any_of(order, [](std::uint32_t word) { return word != 0u && word != KeptSampleOrder; }); +} + +std::uint32_t InvertSampleOrder(std::uint32_t order) { + std::uint32_t inverse = 0; + for (std::uint32_t sample = 0; sample < 8u; ++sample) inverse |= sample << (((order >> (sample * 4u)) & 0x7u) * 4u); + return inverse; +} + +std::uint32_t HostSampleBits(std::uint32_t guestBits, std::uint32_t order) { + std::uint32_t host = 0; + for (std::uint32_t sample = 0; sample < 8u; ++sample) host |= ((guestBits >> ((order >> (sample * 4u)) & 0x7u)) & 1u) << sample; + return host | (guestBits & ~0xffu); +} + +bool SampleMaskFollowsOrder(const State& state, const SampleOrder& order) { + if (!ReordersSamples(order) || !PartialSampleMask(state) || !UniformSampleMask(state)) return false; + const auto full = (1u << state.samples) - 1u; + const auto mask = state.quadSampleMask[0] & full; + return std::ranges::any_of(order, [&](std::uint32_t word) { return (HostSampleBits(mask, word) & full) != mask; }); +} + +std::array QuadSampleMasks(const State& state, const SampleOrder& order) { + if (!SampleMaskFollowsOrder(state, order)) return QuadSampleMasks(state); + const auto mask = static_cast(state.quadSampleMask[0] & ((1u << state.samples) - 1u)); + return {mask, mask, mask, mask}; +} + std::array SampleLocations(const State& state) { Require(state.samples == 1 || state.samples == 2 || state.samples == 4 || state.samples == 8, "invalid sample count"); std::array locations{}; @@ -1160,7 +1255,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { const bool clear = renderControl != cx.end() && (renderControl->second & 3u) != 0; const auto tests = planeTests(cx, depthControl); if ((clear || tests != 0) && depthSurfaceBound(cx)) { - decodeDepth(cx, depthControl, (read(cx, 0x203) & 2u) != 0 && !PixelProgramSkipped(queue), result); + decodeDepth(cx, depthControl, (read(cx, 0x203) & 2u) != 0 && !PixelProgramSkipped(queue), (read(cx, 0x203) & 4u) != 0 && !PixelProgramSkipped(queue), result); } else if (depthPassThrough(depthControl) || ((depthControl & 0xbu) != 0 && (tests == 0 || depthPlanesAbsent(cx)))) { static bool reported = false; if (!reported) { @@ -1244,10 +1339,11 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { // explicitly; KEEP_TOGETHER_ENABLE (bit 24) only affects primitive distribution across the chip. const bool pointsOrLines = result.topology == VK_PRIMITIVE_TOPOLOGY_POINT_LIST || result.topology == VK_PRIMITIVE_TOPOLOGY_LINE_LIST || result.topology == VK_PRIMITIVE_TOPOLOGY_LINE_STRIP || (result.stages.mesh.has_value() && result.stages.mesh->outputPrimitiveSize == 2u); const auto lineOffset = (raster & 0x1800u) != 0 && !pointsOrLines ? 0x2000u : 0u; - const auto rasterMode = raster & ~0x7u & ~(1u << 24u) & ~0x1800u & ~lineOffset & ~(1u << 19u) & ~(1u << 16u); - if (rasterMode != 0 && rasterMode != 0x240u && rasterMode != 0x248u) throw std::runtime_error("AGC graphics: " + zeroMessage(0x205, raster, "polygon mode, depth bias or nonstandard rasterization")); + const auto rasterMode = raster & ~0x7u & ~0x7f8u & ~(1u << 24u) & ~0x1800u & ~lineOffset & ~(1u << 19u) & ~(1u << 16u); + if (rasterMode != 0) throw std::runtime_error("AGC graphics: " + zeroMessage(0x205, raster, "depth bias or nonstandard rasterization")); result.cullMode = ((raster & 1u) != 0 ? VK_CULL_MODE_FRONT_BIT : 0u) | ((raster & 2u) != 0 ? VK_CULL_MODE_BACK_BIT : 0u); if (result.rectList) result.cullMode = VK_CULL_MODE_NONE; + result.polygonMode = decodePolygonMode(cx, raster, result, pointsOrLines); if ((raster & 0x1800u) != 0 && result.depth) decodeDepthBias(cx, raster, result); result.frontFace = (raster & 4u) != 0 ? VK_FRONT_FACE_CLOCKWISE : VK_FRONT_FACE_COUNTER_CLOCKWISE; result.provokingVertexMode = (raster & (1u << 19u)) != 0 ? VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT : VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT; @@ -1326,9 +1422,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)); @@ -1648,7 +1743,13 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot, bool allo } if (!volume && lastSlice != slice) { const auto exports = find(cx, 0x207); - Require(exports == cx.end() || (exports->second & (1u << 18u)) == 0, "layered draws into a color view of several array slices are unsupported"); + if (exports != cx.end() && (exports->second & (1u << 18u)) != 0) { + if (color.arrayLastSlice == 0) { + color.arrayLastSlice = lastSlice; + color.arraySlice = slice; + } + color.layers = lastSlice + 1u - slice; + } } color.extent = {((attrib2 >> 14u) & 0x3fffu) + 1u, (attrib2 & 0x3fffu) + 1u}; color.elementBytes = decoded.elementBytes; @@ -1956,7 +2057,9 @@ std::string NullPixelProgramRejection(const QueueState& queue) { const auto targetMask = find(queue.context, 0x8e); const auto shaderMask = find(queue.context, 0x8f); if (targetMask == queue.context.end() || shaderMask == queue.context.end()) return "AGC graphics: a draw without a pixel program needs CB_TARGET_MASK and CB_SHADER_MASK"; - if (ColorWriteMask(queue.context) != 0) return "AGC graphics: a draw without a pixel program writes color"; + const auto colorControl = find(queue.context, 0x202); + const bool colorDisabled = colorControl != queue.context.end() && ((colorControl->second >> 4u) & 7u) == 0u; + if (!colorDisabled && ColorWriteMask(queue.context) != 0) return "AGC graphics: a draw without a pixel program writes color"; return {}; } @@ -1979,6 +2082,22 @@ bool SplitsAlternateAlpha(const State& state) { return false; } +bool StencilOperationValueReplaces(const Registers& context) { + const auto value = [&](std::uint32_t offset) { + const auto found = find(context, offset); + return found == context.end() ? 0u : found->second; + }; + if ((value(0x203) & 6u) != 4u) return false; + const auto depthControl = value(0x200); + if ((depthControl & 1u) == 0 || (value(0x011) & 1u) == 0 || (value(0x002) & 0x02000000u) != 0 || (value(0x000) & 2u) != 0) return false; + const auto replaces = [](std::uint32_t ops, std::uint32_t refMask) { + if (((refMask >> 16u) & 0xffu) == 0) return false; + return (ops & 0xfu) == 4u || ((ops >> 4u) & 0xfu) == 4u || ((ops >> 8u) & 0xfu) == 4u; + }; + const auto ops = value(0x10b); + return replaces(ops, value(0x10c)) || ((depthControl & 0x80u) != 0 && replaces(ops >> 12u, value(0x10d))); +} + bool StencilReferenceUnused(const State& state) { if (!state.depth || !state.stencilTest) return true; if (state.depthTest && (state.depthCompare < VK_COMPARE_OP_NEVER || state.depthCompare > VK_COMPARE_OP_ALWAYS)) return false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 9e15260b3e..a70da26def 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -1,7 +1,9 @@ #include "prx/libSceAgcDriver/Execution/include/AsyncLog.hpp" #include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" #include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/SlimMutex.hpp" #include "prx/libc/include/General.hpp" #include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" @@ -33,11 +35,23 @@ #include #include #include +#include #include #include namespace AgcDriver::Graphics { +OwnedImage::~OwnedImage() { + if (image) context.Function("vkDestroyImage")(context.device, image, nullptr); + ReleaseImageMemory(context, memory, memoryBytes, memoryType); +} + +void OwnedImage::Allocate(const Context& device, const VkMemoryAllocateInfo& allocation, std::uint32_t mask, const char* operation) { + memory = AllocateImageMemory(device, allocation, mask, operation, &memoryType); + memoryBytes = allocation.allocationSize; + context.bufferPool = device.bufferPool; +} + namespace { // APS5_PROFILE_DRAW: accumulate texture setup phases and report every 200 textures. @@ -88,12 +102,13 @@ thread_local const char* uploadReason = "first"; // (DiscardPendingInside) and units an alias's own results superseded (markLayersPending); relaxed, // reported only. std::atomic singlePassMoves{0}, slabRetiles{0}; -std::atomic partialUploads{0}, partialUploadBytes{0}, partialWriteBacks{0}, partialWriteBackBytes{0}, emptyWriteBacks{0}, coalescedWriteBacks{0}, unitsDropped{0}, unitsSuperseded{0}; +std::atomic partialUploads{0}, partialUploadBytes{0}, partialWriteBacks{0}, partialWriteBackBytes{0}, emptyWriteBacks{0}, coalescedWriteBacks{0}, unitsDropped{0}, unitsSuperseded{0}, clearHandOvers{0}, handedOverSuperseded{0}; // FlushPending listings skipped because no pending unit of the image lay inside the range, images // whose selected pending units were dropped because their memory is no longer registered (see // writeBackLayers), and pending units a DCC clear -> uncompressed key flip kept as the texels // (Refresh). std::atomic pretestSkipped{0}, unregisteredDropped{0}, keyFlipKept{0}; +std::atomic queuedReadHeld{0}; // Images stored by a FlushPending after a hook skip of theirs (AccessKeptByCpu), of which by the // hook for the read site of the last skip; images evictStale dropped (the [hooksync] and [storage] lines). std::atomic flushedAfterSkip{0}, flushedAfterSkipSameSite{0}, staleEvicted{0}; @@ -140,7 +155,7 @@ void reportStorageTraffic(StorageTraffic& traffic) { const auto take = [](std::atomic& counter) { return static_cast(counter.exchange(0, std::memory_order_relaxed)); }; const auto partialUploadCount = take(partialUploads), partialWriteBackCount = take(partialWriteBacks); const auto uploadMiB = take(partialUploadBytes) / 1048576.0, writeBackMiB = take(partialWriteBackBytes) / 1048576.0; - AgcDriver::ProfilePrint_nid_no_patch( "[storage] uploads by path (count/MiB, 10 s):%s; %llu write-backs GPU-direct, %llu stored nothing; block units: partial uploads %llu/%.1f, partial write-backs %llu/%.1f, %llu widened to every pending unit, units dropped %llu, superseded %llu, dropped unregistered %llu, kept over a key flip %llu%s, pretest skipped %llu, evicted stale %llu\n", line.c_str(), static_cast(traffic.directWriteBacks), take(emptyWriteBacks), partialUploadCount, uploadMiB, partialWriteBackCount, writeBackMiB, take(coalescedWriteBacks), take(unitsDropped), take(unitsSuperseded), take(unregisteredDropped), take(keyFlipKept), ShadowReport().c_str(), take(pretestSkipped), take(staleEvicted)); + AgcDriver::ProfilePrint_nid_no_patch( "[storage] uploads by path (count/MiB, 10 s):%s; %llu write-backs GPU-direct, %llu stored nothing; block units: partial uploads %llu/%.1f, partial write-backs %llu/%.1f, %llu widened to every pending unit, units dropped %llu, superseded %llu, handed over at %llu clear-key refreshes (%llu units superseded), dropped unregistered %llu, kept over a key flip %llu%s, pretest skipped %llu, held for a queued draw read %llu, evicted stale %llu\n", line.c_str(), static_cast(traffic.directWriteBacks), take(emptyWriteBacks), partialUploadCount, uploadMiB, partialWriteBackCount, writeBackMiB, take(coalescedWriteBacks), take(unitsDropped), take(unitsSuperseded), take(clearHandOvers), take(handedOverSuperseded), take(unregisteredDropped), take(keyFlipKept), ShadowReport().c_str(), take(pretestSkipped), take(queuedReadHeld), take(staleEvicted)); traffic.writeBacks.clear(); traffic.uploadReasons.clear(); traffic.directWriteBacks = 0; @@ -265,7 +280,7 @@ ExpandImage CreateExpandImage(const Context& context, const GuestTextureResource VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; allocation.allocationSize = requirements.size; allocation.memoryTypeIndex = context.DeviceMemoryType(requirements.memoryTypeBits, requirements.size, "texture"); - result.owned->memory = AllocateImageMemory(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory sRGB copy"); + result.owned->Allocate(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory sRGB copy"); Check(context.Function("vkBindImageMemory")(context.device, result.image, result.owned->memory, 0), "vkBindImageMemory sRGB copy"); result.bytes = requirements.size; return result; @@ -326,12 +341,14 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex Require(arrayLayers == 0 || sliceLinearBytes <= UINT64_MAX / arrayLayers, "detiled texture buffer size overflows"); const auto linearBytes = sliceLinearBytes * arrayLayers; + const bool depth1D = depthCompare && (descriptor.dimension == TextureDimension::k1D || descriptor.dimension == TextureDimension::k1DArray); VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; imageInfo.flags = (descriptor.dimension == TextureDimension::kCube ? VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT : 0u) | (SrgbTwinFormat(context, vkFormat) != VK_FORMAT_UNDEFINED ? VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT : 0u); - imageInfo.imageType = ImageTypeFor(descriptor.dimension); + imageInfo.imageType = depth1D ? VK_IMAGE_TYPE_2D : ImageTypeFor(descriptor.dimension); imageInfo.format = vkFormat; - imageInfo.extent = {descriptor.width, descriptor.height, geometry.imageDepth}; + imageInfo.extent = {descriptor.width, depth1D ? 1u : descriptor.height, geometry.imageDepth}; imageInfo.mipLevels = descriptor.mipCount; + Require(geometry.imageLayers <= context.limits.maxImageArrayLayers, "a texture array with more layers than maxImageArrayLayers is not implemented"); imageInfo.arrayLayers = geometry.imageLayers; imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; @@ -347,7 +364,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex allocation.allocationSize = requirements.size; allocationBytes = requirements.size; allocation.memoryTypeIndex = context.DeviceMemoryType(requirements.memoryTypeBits, requirements.size, "texture"); - owned->memory = AllocateImageMemory(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory texture"); + owned->Allocate(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory texture"); Check(context.Function("vkBindImageMemory")(context.device, image, owned->memory, 0), "vkBindImageMemory"); const VkImage uploadImage = image; @@ -522,12 +539,12 @@ 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}; viewInfo.image = image; - viewInfo.viewType = ViewTypeFor(descriptor.dimension, viewLayerCount); + viewInfo.viewType = depth1D ? (descriptor.dimension == TextureDimension::k1DArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D) : ViewTypeFor(descriptor.dimension, viewLayerCount); viewInfo.format = srgbExpand ? VK_FORMAT_R8G8B8A8_SRGB : vkFormat; viewFormat = viewInfo.format; viewInfo.components = depthCompare ? VkComponentMapping{} : components; @@ -623,13 +640,13 @@ Texture::Texture(const Context& context, const std::shared_ptr& allocation.allocationSize = requirements.size; allocationBytes = requirements.size; allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - owned->memory = AllocateImageMemory(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory repacked copy"); + owned->Allocate(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory repacked copy"); Check(context.Function("vkBindImageMemory")(context.device, image, owned->memory, 0), "vkBindImageMemory repacked copy"); 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; @@ -681,7 +698,7 @@ Texture::Texture(const Context& context, const std::shared_ptr& allocation.allocationSize = requirements.size; allocationBytes = requirements.size; allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - owned->memory = AllocateImageMemory(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory block copy"); + owned->Allocate(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory block copy"); Check(context.Function("vkBindImageMemory")(context.device, image, owned->memory, 0), "vkBindImageMemory block copy"); const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u; @@ -718,11 +735,12 @@ Texture::Texture(const Context& context, const std::shared_ptr& copyElementBytes = BytesPerElement(descriptor.format); AGC_LOG_DEFERRED(stdout, "Texture address=0x%llx %ux%u mips=%u layers=%u depth copy of storage image (vk=%d -> %d)", static_cast(descriptor.baseAddress), descriptor.width, descriptor.height, descriptor.mipCount, copyLayers, static_cast(colorFormat), static_cast(depthFormat)); + const bool depth1D = descriptor.dimension == TextureDimension::k1D || descriptor.dimension == TextureDimension::k1DArray; VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; imageInfo.flags = descriptor.dimension == TextureDimension::kCube ? VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT : 0u; - imageInfo.imageType = ImageTypeFor(descriptor.dimension); + imageInfo.imageType = depth1D ? VK_IMAGE_TYPE_2D : ImageTypeFor(descriptor.dimension); imageInfo.format = depthFormat; - imageInfo.extent = {descriptor.width, descriptor.height, 1u}; + imageInfo.extent = {descriptor.width, depth1D ? 1u : descriptor.height, 1u}; imageInfo.mipLevels = copyLevels; imageInfo.arrayLayers = copyLayers; imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; @@ -738,24 +756,26 @@ Texture::Texture(const Context& context, const std::shared_ptr& allocation.allocationSize = requirements.size; allocationBytes = requirements.size; allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - owned->memory = AllocateImageMemory(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory depth copy"); + owned->Allocate(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory depth copy"); Check(context.Function("vkBindImageMemory")(context.device, image, owned->memory, 0), "vkBindImageMemory depth copy"); 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; - viewInfo.viewType = ViewTypeFor(descriptor.dimension, viewLayerCount); + viewInfo.viewType = depth1D ? (descriptor.dimension == TextureDimension::k1DArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D) : ViewTypeFor(descriptor.dimension, viewLayerCount); viewInfo.format = depthFormat; viewInfo.components = VkComponentMapping{}; viewInfo.subresourceRange = {VK_IMAGE_ASPECT_DEPTH_BIT, descriptor.baseLevel, viewLevelCount, descriptor.baseArray, viewLayerCount}; VkImageViewMinLodCreateInfoEXT minLod{VK_STRUCTURE_TYPE_IMAGE_VIEW_MIN_LOD_CREATE_INFO_EXT}; ChainMinLod(context, descriptor, viewInfo, minLod); Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView depth copy"); + viewRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount}; createFirstLayerView(descriptor, viewInfo); + prepareBaseLevelView(descriptor, viewInfo); static_cast(components); depthCopyTextures.fetch_add(1, std::memory_order_relaxed); } catch (...) { @@ -1011,7 +1031,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(); @@ -1039,10 +1059,13 @@ Texture::Texture(const Context& context, const std::shared_ptr& } } -Texture::Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components, VkImageViewType viewType, std::uint32_t baseLayer, std::shared_ptr owner) : context(context), owned(std::move(owner)) { +Texture::Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components, VkImageViewType viewType, std::uint32_t baseLayer, std::shared_ptr owner, VkImageUsageFlags usage) : context(context), owned(std::move(owner)) { Require(owned == nullptr || (owned->image == depthImage && owned->context.device == context.device), "a depth sampled view must retain its own device and image"); layout = VK_IMAGE_LAYOUT_GENERAL; VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + VkImageViewUsageCreateInfo viewUsage{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO}; + viewUsage.usage = usage; + if (usage != 0) viewInfo.pNext = &viewUsage; viewInfo.image = depthImage; viewInfo.viewType = viewType; viewInfo.format = depthFormat; @@ -1063,7 +1086,7 @@ Texture::~Texture() { } void Texture::createFirstLayerView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo) { - if (descriptor.dimension != TextureDimension::k2DArray) return; + if (descriptor.dimension != TextureDimension::k2DArray && (viewInfo.viewType != VK_IMAGE_VIEW_TYPE_2D_ARRAY || descriptor.dimension != TextureDimension::k1DArray)) return; viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; viewInfo.subresourceRange.layerCount = 1; Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &firstLayerView), "vkCreateImageView first layer"); @@ -1072,9 +1095,10 @@ void Texture::createFirstLayerView(const GuestTextureResource& descriptor, VkIma void Texture::prepareBaseLevelView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo) { if (viewInfo.subresourceRange.levelCount == 1u) return; - if (descriptor.dimension != TextureDimension::k1D && descriptor.dimension != TextureDimension::k2D && descriptor.dimension != TextureDimension::k2DArray) return; + const bool depth1D = (viewInfo.subresourceRange.aspectMask & VK_IMAGE_ASPECT_DEPTH_BIT) && (descriptor.dimension == TextureDimension::k1D || descriptor.dimension == TextureDimension::k1DArray); + if (!depth1D && descriptor.dimension != TextureDimension::k1D && descriptor.dimension != TextureDimension::k2D && descriptor.dimension != TextureDimension::k2DArray) return; viewInfo.pNext = nullptr; - viewInfo.viewType = descriptor.dimension == TextureDimension::k1D ? VK_IMAGE_VIEW_TYPE_1D : VK_IMAGE_VIEW_TYPE_2D; + viewInfo.viewType = (descriptor.dimension == TextureDimension::k1D && !depth1D) ? VK_IMAGE_VIEW_TYPE_1D : VK_IMAGE_VIEW_TYPE_2D; viewInfo.subresourceRange.levelCount = 1; viewInfo.subresourceRange.layerCount = 1; baseLevelInfo = viewInfo; @@ -1387,6 +1411,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; @@ -1406,7 +1431,7 @@ StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler, allocation.allocationSize = requirements.size; memoryBytes = requirements.size; allocation.memoryTypeIndex = context.DeviceMemoryType(requirements.memoryTypeBits, requirements.size, "storage texture"); - memory = AllocateImageMemory(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory storage texture"); + memory = AllocateImageMemory(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory storage texture", &memoryType); Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory storage"); } catch (...) { release(); @@ -1488,6 +1513,7 @@ struct PendingWrites { PendingList textures; // Images taken out of `textures` by a FlushPending still storing them (see adjacentPendingUnchanged). std::vector flushing; + std::atomic flushingCount{0}; }; PendingWrites& Pending() { @@ -1818,8 +1844,9 @@ VkImageView StorageTexture::AttachmentView(VkFormat format, std::uint32_t mip, s Require(attachable, "storage image cannot be a color attachment"); Require(mip < descriptor.mipCount, "attachment mip exceeds the storage image"); const bool volume = descriptor.dimension == TextureDimension::k3D; - Require((depthSlice == 0 && layers == 1) || (volume && mip == 0 && layers != 0 && depthSlice + layers - 1u <= descriptor.depthOrLastArray), "attachment slice is outside the storage image"); - Require(layer == 0 || (!volume && descriptor.dimension == TextureDimension::k2DArray && descriptor.baseArray + layer <= descriptor.depthOrLastArray), "attachment layer is outside the storage image"); + const bool array = !volume && descriptor.dimension == TextureDimension::k2DArray; + Require((depthSlice == 0 && layers == 1) || (volume && mip == 0 && layers != 0 && depthSlice + layers - 1u <= descriptor.depthOrLastArray) || (array && depthSlice == 0 && layers != 0), "attachment slice is outside the storage image"); + Require((layer == 0 && layers == 1) || (volume && layer == 0) || (array && descriptor.baseArray + layer + layers - 1u <= descriptor.depthOrLastArray), "attachment layer is outside the storage image"); const auto found = attachmentViews.find({format, mip, depthSlice, layers, layer}); if (found != attachmentViews.end()) return found->second; VkImageViewUsageCreateInfo usage{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO}; @@ -2049,7 +2076,10 @@ bool StorageTexture::Refresh() { } } // No publish: the upload below reads the stored units from the unit shadow itself. - const bool flushed = FlushPending(descriptor.baseAddress, static_cast(guestBytes), alias.get(), "storage refresh", PublishScope::None); + handedOver.clear(); + handedUnits.clear(); + const auto handOverKeys = alias == nullptr ? clearHandOver() : std::nullopt; + const bool flushed = !handOverKeys.has_value() && FlushPending(descriptor.baseAddress, static_cast(guestBytes), alias.get(), "storage refresh", PublishScope::None); if (profile && flushed) start = LookupOutcomes::Add(LookupOutcomes::PendingFlush, start); // `original` holds the guest bytes the image was last uploaded from or written back as; while the // guest memory and the DCC keys still match, the image content is current. Pages nobody wrote @@ -2094,7 +2124,7 @@ bool StorageTexture::Refresh() { } }; const auto keysStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; - keys = ProvedKeys(); + keys = handOverKeys.has_value() ? *handOverKeys : ProvedKeys(); if (profile && descriptor.dccAddress != 0) LookupOutcomes::Add(LookupOutcomes::DccScan, keysStart); if (keys != uploadedKeys) { changed.assign(trackedLayers, true); @@ -2919,7 +2949,7 @@ std::vector StorageTexture::importSegments() cons for (const auto& piece : pieces) { const auto from = std::max(piece.address, begin); const auto to = std::min(piece.address + piece.bytes, end); - const auto* import = HostImportFor(context, from, static_cast(to - from)); + const auto* import = HostImportFor(context, from, static_cast(to - from), ImportRequester::Segment); if (import == nullptr) return {}; if (!segments.empty() && segments.back().import == import) segments.back().end = to; else if (!segments.empty() && unitAligned && from % 65536 != 0) return {}; @@ -3108,6 +3138,10 @@ std::uint64_t StorageTexture::writeBackWindows(std::span se } if (runs.empty()) return 0; auto windows = sliceWindows(runs); + // A kept unit can lie outside every window: the tracking grid is flat over the guest range + // while the slices of a 3D surface taper in depth with the level (HasLayer), so the upper + // depth slices' higher-level blocks belong to no window. There is no tiled data to retile. + if (windows.empty()) return 0; // The linear rows and the tiled bytes of every window, each at a 256-byte position; the tail // levels of a layer retile into one scratch region (their block is shared). std::vector linearPositions, scratchPositions; @@ -3511,14 +3545,14 @@ void StorageTexture::MarkDirty() { markLayersPending(0, trackedLayers); } -void StorageTexture::MarkSliceDirty(std::uint32_t slice) { - if (geometry.thick || geometry.imageLayers <= 1 || slice >= geometry.imageLayers || geometry.layerBytes == 0 || trackedLayerBytes == 0 || descriptor.dccAddress != 0) { +void StorageTexture::MarkSliceDirty(std::uint32_t slice, std::uint32_t count) { + if (geometry.thick || geometry.imageLayers <= 1 || count == 0 || slice >= geometry.imageLayers || count > geometry.imageLayers - slice || geometry.layerBytes == 0 || trackedLayerBytes == 0 || descriptor.dccAddress != 0) { MarkDirty(); return; } const auto begin = geometry.GuestLayerOffset(slice); const auto first = static_cast(begin / trackedLayerBytes); - const auto last = static_cast(std::min((begin + geometry.layerBytes + trackedLayerBytes - 1) / trackedLayerBytes, trackedLayers)); + const auto last = static_cast(std::min((begin + geometry.layerBytes * count + trackedLayerBytes - 1) / trackedLayerBytes, trackedLayers)); if (first >= last) { MarkDirty(); return; @@ -3536,6 +3570,7 @@ void StorageTexture::refreshGeneration() { void StorageTexture::markLayersPending(std::uint32_t first, std::uint32_t count) { CaptureTrace::Log("image-write image=%llx first=%u count=%u generation=%llu", static_cast(descriptor.baseAddress), first, count, static_cast(generation)); + ++writes; static const bool eager = std::getenv("APS5_EAGER_WRITEBACK") != nullptr || std::getenv("APS5_NO_TEXTURE_CACHE") != nullptr; std::vector unsaved(trackedLayers, false); bool anyUnsaved = false; @@ -3560,6 +3595,7 @@ void StorageTexture::markLayersPending(std::uint32_t first, std::uint32_t count) } if (superseded) source->reconcilePending(); } + if (!handedOver.empty() || !handedUnits.empty()) supersedeHandedOver(first, count); if (eager) { WriteBack(); return; @@ -3639,7 +3675,12 @@ bool StorageTexture::pendingUnitInside(std::uint64_t address, std::size_t bytes) } bool StorageTexture::FlushPending(std::uint64_t address, std::size_t bytes, const StorageTexture* except, const char* reason, PublishScope scope, bool* published) { + static_cast(FlushDepthPlanes(address, bytes)); if (published != nullptr) *published = false; + if (AgcDriver::DriverDetail::DrawPipeline::HeldReads(address, bytes)) { + queuedReadHeld.fetch_add(1, std::memory_order_relaxed); + return false; + } // The images stay alive across the scan: their last owner may be a batch's kept list, which // another thread releases outside the GPU mutex once the batch completed. std::vector> flush; @@ -3668,14 +3709,16 @@ bool StorageTexture::FlushPending(std::uint64_t address, std::size_t bytes, cons } // Still pending for the adjacency rule of writeBack until each one is stored. for (const auto& texture : flush) pending.flushing.push_back(texture.get()); + pending.flushingCount.store(pending.flushing.size(), std::memory_order_release); if (!flush.empty()) BumpPendingSerial(); } if (flush.empty()) { + const bool waited = WaitForFlushing(address, bytes, except); // Units shadowed over the range (another image's results retiled into the import's unit // shadow, or an earlier flush's) still reach the import as the scope asks. const bool units = PublishShadowsOnly(address, bytes, scope, reason); if (published != nullptr) *published = units; - return false; + return waited; } struct Unregister { const std::vector>& flush; @@ -3683,6 +3726,7 @@ bool StorageTexture::FlushPending(std::uint64_t address, std::size_t bytes, cons auto& pending = Pending(); std::lock_guard lock(pending.mutex); for (const auto& texture : flush) std::erase(pending.flushing, texture.get()); + pending.flushingCount.store(pending.flushing.size(), std::memory_order_release); BumpPendingSerial(); } } unregister{flush}; @@ -3728,6 +3772,20 @@ bool StorageTexture::FlushPending(std::uint64_t address, std::size_t bytes, cons return true; } +bool StorageTexture::WaitForFlushing(std::uint64_t address, std::size_t bytes, const StorageTexture* except) { + auto& pending = Pending(); + if (pending.flushingCount.load(std::memory_order_acquire) == 0 || GuestMemory::GpuMutex().HeldByThisThread()) return false; + const auto* exempt = refreshing; + for (bool waited = false;; waited = true) { + { + std::lock_guard lock(pending.mutex); + if (std::none_of(pending.flushing.begin(), pending.flushing.end(), [&](const StorageTexture* texture) { return texture != except && texture != exempt && texture->overlaps(address, bytes); })) return waited; + } + { std::lock_guard gpu(GuestMemory::GpuMutex()); } + std::this_thread::yield(); + } +} + void StorageTexture::SaveBeforeUnwatch(std::uint64_t address, std::size_t bytes) { std::vector> overlapping; { @@ -4662,28 +4720,19 @@ bool StorageTexture::SameSurfaceShape(const StorageTexture& other) const { return guestBytes == other.guestBytes && mine.width == theirs.width && mine.height == theirs.height && mine.depthOrLastArray == theirs.depthOrLastArray && mine.baseArray == theirs.baseArray && mine.mipCount == theirs.mipCount && mine.tileMode == theirs.tileMode && mine.dimension == theirs.dimension && mine.format == theirs.format && storageFormat == other.storageFormat && geometry.imageLayers == other.geometry.imageLayers && geometry.imageDepth == other.geometry.imageDepth; } +const char* StorageTexture::CopyRefusal(const Context& context, const StorageTexture* destination, const StorageTexture& source, std::uint64_t address) { + if (destination != nullptr && (destination == &source || !destination->SameSurfaceShape(source))) return "shape"; + if (source.descriptor.dccAddress != 0 || (destination != nullptr && destination->descriptor.dccAddress != 0)) return "keys"; + if (source.released || !source.Cached() || (destination != nullptr && (destination->released || !destination->Cached()))) return "released"; + if (HostImportFor(context, address, static_cast(source.guestBytes)) == nullptr) return "not imported"; + if (Recorder::Active() == nullptr) return "no recorder"; + return nullptr; +} + bool StorageTexture::CopyFrom(StorageTexture& source, const char*& refusal) { - if (&source == this || !SameSurfaceShape(source)) { - refusal = "shape"; - return false; - } - if (descriptor.dccAddress != 0 || source.descriptor.dccAddress != 0) { - refusal = "keys"; - return false; - } - if (released || source.released || !Cached() || !source.Cached()) { - refusal = "released"; - return false; - } - if (HostImportFor(context, descriptor.baseAddress, static_cast(guestBytes)) == nullptr) { - refusal = "not imported"; - return false; - } + refusal = CopyRefusal(context, this, source, descriptor.baseAddress); + if (refusal != nullptr) return false; auto* recorder = Recorder::Active(); - if (recorder == nullptr) { - refusal = "no recorder"; - return false; - } // The source image must hold the surface: a stale one uploads first (and other images' // results pending over its memory land first, as the transfer's flush would have them). source.Refresh(); @@ -4744,6 +4793,11 @@ bool StorageTexture::CopyFrom(StorageTexture& source, const char*& refusal) { copied.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &copied); recorder->EndGpuTiming(timing, guestBytes); + MarkWholeWritten(); + return true; +} + +void StorageTexture::MarkWholeWritten() { // As after a clear: the image holds the surface, guest memory follows at the write-back (which // keeps the 64 KiB blocks the CPU writes from now on; the collect is uncached for the reason // given in FillClear). @@ -4758,7 +4812,6 @@ bool StorageTexture::CopyFrom(StorageTexture& source, const char*& refusal) { refreshGeneration(); forgetBorrowed(0, trackedLayers); MarkDirty(); - return true; } // See AdjacentGenerationEnabled above. @@ -4830,6 +4883,104 @@ void StorageTexture::blockGenerations(std::vector& generations) c } } +namespace { + +std::atomic& ClearHandOverOn() { + static std::atomic on{std::getenv("APS5_NO_CLEAR_HAND_OVER") == nullptr}; + return on; +} + +} + +void StorageTexture::SetClearHandOver(bool enabled) { + ClearHandOverOn().store(enabled, std::memory_order_relaxed); +} + +std::uint64_t StorageTexture::ClearHandOvers() { + return clearHandOvers.load(std::memory_order_relaxed); +} + +std::uint64_t StorageTexture::HandedOverSuperseded() { + return handedOverSuperseded.load(std::memory_order_relaxed); +} + +std::optional StorageTexture::clearHandOver() { + if (!ClearHandOverOn().load(std::memory_order_relaxed) || !blockUnits || descriptor.dccAddress == 0 || anyLayerPending()) return std::nullopt; + if (!GuestMemory::Watched(descriptor.baseAddress, static_cast(guestBytes))) return std::nullopt; + const auto others = overlappingPending(descriptor.baseAddress, static_cast(guestBytes)); + if (others.empty()) return std::nullopt; + const auto begin = descriptor.baseAddress; + const auto end = descriptor.baseAddress + guestBytes; + const auto keyBytes = DccKeyBytes(guestBytes); + for (const auto& other : others) { + if (!other->blockUnits || (other->descriptor.dccAddress != 0 && other->uploadedKeys != DccKeys::Uncompressed)) return std::nullopt; + if (other->descriptor.dccAddress != 0 && other->descriptor.dccAddress < descriptor.dccAddress + keyBytes && descriptor.dccAddress < other->descriptor.dccAddress + DccKeyBytes(other->guestBytes)) return std::nullopt; + for (std::uint32_t unit = 0; unit < other->trackedLayers; ++unit) { + if (!other->layerPending[unit]) continue; + const auto from = other->layerBegin(unit); + const auto to = from + other->layerBytes(unit); + if (to <= begin || from >= end) continue; + if (from < begin || to > end || other->layerBytes(unit) != trackedLayerBytes) return std::nullopt; + } + } + const auto keys = ProvedKeys(); + VkClearColorValue clear{}; + if (!IsDccClear(keys) || !ClearColorFor(storageFormat, keys, clear)) return std::nullopt; + if (handedUnits.size() != trackedLayers) handedUnits.assign(trackedLayers, false); + for (const auto& other : others) { + for (std::uint32_t unit = 0; unit < other->trackedLayers; ++unit) { + const auto from = other->layerBegin(unit); + if (other->layerPending[unit] && from >= begin && from < end) handedUnits[static_cast((from - begin) / trackedLayerBytes)] = true; + } + const auto same = std::find_if(handedOver.begin(), handedOver.end(), [&](const auto& entry) { return entry.first.lock() == other; }); + if (same != handedOver.end()) same->second = other->writes; + else handedOver.emplace_back(other, other->writes); + } + clearHandOvers.fetch_add(1, std::memory_order_relaxed); + return keys; +} + +void StorageTexture::supersedeHandedOver(std::uint32_t first, std::uint32_t count) { + const auto begin = layerBegin(first); + const auto end = first + count >= trackedLayers ? descriptor.baseAddress + guestBytes : layerBegin(first + count); + for (auto entry = handedOver.begin(); entry != handedOver.end();) { + const auto source = entry->first.lock(); + if (source == nullptr || source->writes != entry->second) { + entry = handedOver.erase(entry); + continue; + } + bool superseded = false; + for (std::uint32_t unit = 0; unit < source->trackedLayers; ++unit) { + const auto from = source->layerBegin(unit); + if (!source->layerPending[unit] || from < begin || from + source->layerBytes(unit) > end) continue; + source->layerPending[unit] = false; + handedOverSuperseded.fetch_add(1, std::memory_order_relaxed); + superseded = true; + } + if (superseded) source->reconcilePending(); + ++entry; + } + if (first == 0 && count >= trackedLayers) handedOver.clear(); + if (std::any_of(handedUnits.begin() + first, handedUnits.begin() + std::min(first + count, handedUnits.size()), [](bool handed) { return handed; })) { + const auto current = GuestMemory::CollectWrites(descriptor.baseAddress, static_cast(guestBytes)); + std::vector generations; + blockGenerations(generations); + std::vector stamped(generations.size(), 0); + std::vector cpu(generations.size(), 0); + const bool tracked = GuestMemory::ChangedBlocks(descriptor.baseAddress, static_cast(guestBytes), generations, stamped, cpu); + bool moved = false; + for (std::uint32_t unit = first; unit < first + count && unit < handedUnits.size(); ++unit) { + if (!handedUnits[unit]) continue; + handedUnits[unit] = false; + if (!tracked || unit >= cpu.size() || cpu[unit] != 0) continue; + layerGeneration[unit] = current; + moved = true; + } + if (moved) refreshGeneration(); + } + if (std::none_of(handedUnits.begin(), handedUnits.end(), [](bool handed) { return handed; })) handedUnits.clear(); +} + std::shared_ptr StorageTexture::pendingAlias() const { static const bool disabled = std::getenv("APS5_NO_ALIAS_BORROW") != nullptr; if (disabled || !blockUnits) return nullptr; @@ -5462,7 +5613,7 @@ void StorageTexture::release() noexcept { if (proxyMemory) context.Function("vkFreeMemory")(context.device, proxyMemory, nullptr); if (view) context.Function("vkDestroyImageView")(context.device, view, nullptr); if (image) context.Function("vkDestroyImage")(context.device, image, nullptr); - if (memory) context.Function("vkFreeMemory")(context.device, memory, nullptr); + ReleaseImageMemory(context, memory, memoryBytes, memoryType); } std::uint64_t StorageTexture::GuestBytes() const { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp index 0bd34f6890..74621bdba1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp @@ -89,10 +89,7 @@ TextureDetiler::TextureDetiler(const Context& context) : context(context) { pipelineLayoutInfo.pushConstantRangeCount = 1; pipelineLayoutInfo.pPushConstantRanges = &pushRange; Check(context.Function("vkCreatePipelineLayout")(context.device, &pipelineLayoutInfo, nullptr, &pipelineLayout), "vkCreatePipelineLayout"); - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = sizeof(TEXTURE_DETILE_SPV); - moduleInfo.pCode = TEXTURE_DETILE_SPV; - Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &module), "vkCreateShaderModule"); + module = CreateShaderModule(context, TEXTURE_DETILE_SPV, sizeof(TEXTURE_DETILE_SPV), "vkCreateShaderModule"); bindings[1].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; Check(context.Function("vkCreateDescriptorSetLayout")(context.device, &layoutInfo, nullptr, &imageDescriptorLayout), "vkCreateDescriptorSetLayout"); pipelineLayoutInfo.pSetLayouts = &imageDescriptorLayout; @@ -168,10 +165,7 @@ VkPipeline TextureDetiler::pipeline(TextureTileMode tileMode, std::uint32_t elem std::copy(equation->bits.begin(), equation->bits.end(), values.begin() + 4); } if (image && imageModule == VK_NULL_HANDLE) { - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = sizeof(TEXTURE_DETILE_IMAGE_SPV); - moduleInfo.pCode = TEXTURE_DETILE_IMAGE_SPV; - Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &imageModule), "vkCreateShaderModule"); + imageModule = CreateShaderModule(context, TEXTURE_DETILE_IMAGE_SPV, sizeof(TEXTURE_DETILE_IMAGE_SPV), "vkCreateShaderModule"); } std::array entries{}; for (std::uint32_t index = 0; index < entries.size(); ++index) entries[index] = {index, index * 4u, 4}; @@ -273,10 +267,7 @@ void TextureDetiler::DispatchRepack(VkCommandBuffer commands, VkBuffer source, s Require(commands != VK_NULL_HANDLE && source != VK_NULL_HANDLE && destination != VK_NULL_HANDLE, "a texel repack needs a command buffer and its buffers"); Require(bytes != 0 && bytes % 4u == 0 && sourceOffset % 4u == 0 && destinationOffset % 4u == 0, "a texel repack needs whole aligned words"); if (repackPipeline == VK_NULL_HANDLE) { - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = sizeof(TEXTURE_REPACK_SPV); - moduleInfo.pCode = TEXTURE_REPACK_SPV; - Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &repackModule), "vkCreateShaderModule repack"); + repackModule = CreateShaderModule(context, TEXTURE_REPACK_SPV, sizeof(TEXTURE_REPACK_SPV), "vkCreateShaderModule repack"); VkPipelineShaderStageCreateInfo stage{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; stage.module = repackModule; @@ -347,11 +338,7 @@ void TextureDetiler::DispatchCmaskClear(VkCommandBuffer commands, VkBuffer cmask pipelineLayoutInfo.pushConstantRangeCount = 1; pipelineLayoutInfo.pPushConstantRanges = &pushRange; Check(context.Function("vkCreatePipelineLayout")(context.device, &pipelineLayoutInfo, nullptr, &cmaskPipelineLayout), "vkCreatePipelineLayout CMASK clear"); - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = sizeof(CMASK_CLEAR_SPV); - moduleInfo.pCode = CMASK_CLEAR_SPV; - VkShaderModule cmaskModule = VK_NULL_HANDLE; - Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &cmaskModule), "vkCreateShaderModule CMASK clear"); + VkShaderModule cmaskModule = CreateShaderModule(context, CMASK_CLEAR_SPV, sizeof(CMASK_CLEAR_SPV), "vkCreateShaderModule CMASK clear"); VkComputePipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; pipelineInfo.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; pipelineInfo.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp index c9617b896f..e667909a5b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp @@ -170,6 +170,14 @@ bool IsSrgbTextureFormat(std::uint32_t guestFormat) { } } +bool IgnoresForcedDegamma(std::uint32_t guestFormat) { + switch (guestFormat) { + case 7u: case 8u: case 13u: case 22u: case 23u: case 24u: case 29u: case 36u: case 50u: case 64u: case 65u: case 66u: case 71u: case 77u: + case 132u: case 133u: case 134u: case 136u: case 175u: case 176u: case 177u: case 178u: case 179u: case 180u: return true; + default: return false; + } +} + std::optional FindGuestTextureFormat(VkFormat format, std::uint32_t elementBytes) { for (const auto& entry : kFormatLookup) { if (!entry.blockCompressed && entry.vkFormat == format && entry.bytesPerElement == elementBytes) return entry.guestFormat; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp index d2fcbf8852..ff1bed0787 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureSwizzleEquations.hpp" #include #include #include @@ -465,6 +466,45 @@ bool LevelsFitAllocation(const GuestTextureResource& surface, std::uint32_t leve return true; } +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; + if (const auto xorMode = XorSwizzleMode(viewed.tileMode); xorMode != 0u) { + const auto* equation = FindTextureSwizzleEquation(xorMode, BytesPerElement(viewed.format)); + if (equation == nullptr) return viewed; + std::uint32_t sliceBits = 0; + for (const auto bit : equation->bits) sliceBits |= bit >> 24u; + const auto sliceMask = sliceBits == 0 ? 0u : (1u << std::bit_width(sliceBits)) - 1u; + if ((viewed.baseArray & sliceMask) != 0) return viewed; + } + const auto geometry = DescribeSurface(viewed); + if (geometry.thick || geometry.layers != viewed.depthOrLastArray + 1u) return viewed; + auto window = viewed; + window.baseAddress = viewed.baseAddress + geometry.GuestLayerOffset(viewed.baseArray); + window.depthOrLastArray = viewed.depthOrLastArray - viewed.baseArray; + window.baseArray = 0; + 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/Resource/src/Registration.cpp b/core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp index fe08199be9..f3fbd0ba67 100644 --- a/core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp +++ b/core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp @@ -150,4 +150,16 @@ int APS5_VABI sceAgcDriverFindResourcesPublic(const void* callback, void* user_d return SCE_AGC_ERROR_RESOURCE_REGISTRATION_UNAVAILABLE; } +int APS5_VABI sceAgcDriverRegisterOwnerForSystem() { + return SCE_AGC_ERROR_RESOURCE_REGISTRATION_UNAVAILABLE; +} + +int APS5_VABI sceAgcDriverInitResourceRegistrationInterface() { + return SCE_AGC_ERROR_RESOURCE_REGISTRATION_UNAVAILABLE; +} + +int APS5_VABI sceAgcDriverSetResourceRegistrationInfo() { + return 0; +} + } diff --git a/core/libs/prx/libSceAgcDriver/Submit/src/Dcb.cpp b/core/libs/prx/libSceAgcDriver/Submit/src/Dcb.cpp index b7b8d2249c..04702130a0 100644 --- a/core/libs/prx/libSceAgcDriver/Submit/src/Dcb.cpp +++ b/core/libs/prx/libSceAgcDriver/Submit/src/Dcb.cpp @@ -22,6 +22,20 @@ int AgcDriver::SubmitMany(std::uint32_t* const* addresses, const std::uint32_t* return 0; } +namespace { + +std::uint32_t PayloadDwords(std::uint32_t bytes) { + return static_cast((std::uint64_t{bytes} + 3) >> 2); +} + +std::uint32_t WorkloadDataPacketDwords(std::uint32_t bytes) { + if (bytes == 0) return 3; + const std::uint32_t payload = PayloadDwords(bytes); + return payload == 1 ? 4 : payload + 7; +} + +} + extern "C" { std::uint32_t APS5_VABI sceAgcDriverGetWaitRenderingPacketSizeInDwords() { @@ -32,6 +46,26 @@ std::uint32_t APS5_VABI sceAgcDriverGetSetFlipPacketSizeInDwords() { return AgcDriver::FlipPacketWords; } +std::uint32_t APS5_VABI sceAgcDriverGetSetWorkloadsActivePacketSize() { + return WorkloadDataPacketDwords(8) + 9; +} + +std::uint32_t APS5_VABI sceAgcDriverGetSetWorkloadCompletePacketSize() { + return WorkloadDataPacketDwords(0) + 9; +} + +std::uint32_t APS5_VABI sceAgcDriverGetSetWorkloadStreamInactivePacketSize() { + return WorkloadDataPacketDwords(8); +} + +std::uint32_t APS5_VABI sceAgcDriverNopDataGetPacketSize(std::uint32_t bytes) { + return PayloadDwords(bytes) + 2; +} + +std::uint32_t APS5_VABI sceAgcDriverUserDataGetPacketSize(std::uint32_t bytes) { + return WorkloadDataPacketDwords(bytes); +} + std::uint32_t APS5_VABI sceAgcDriverWaitUntilSafeForRendering(std::uint32_t** command, std::uint32_t capacity, std::uint32_t mode, std::uint32_t handle, int index) { if (!command || capacity < AgcDriver::RenderingWaitPacketWords || mode != 0 || index < 0) diff --git a/core/libs/prx/libSceAgcDriver/Unimplemented.cpp b/core/libs/prx/libSceAgcDriver/Unimplemented.cpp index ddb13df0a1..be639faf70 100644 --- a/core/libs/prx/libSceAgcDriver/Unimplemented.cpp +++ b/core/libs/prx/libSceAgcDriver/Unimplemented.cpp @@ -1,11 +1,16 @@ #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" namespace { constexpr int DEBUG_UNAVAILABLE = static_cast(0x8A6C1000); +constexpr int DRIVER_FEATURE_UNAVAILABLE = static_cast(0x8A6D0001); +constexpr int DRIVER_NULL_OUTPUT = static_cast(0x8A6D0003); +constexpr int GPU_RESET_UNAVAILABLE = static_cast(0x8A6C0045); +constexpr std::size_t TOOL_MEMORY_RANGE_SIZE = 12; } @@ -52,4 +57,161 @@ int APS5_VABI sceAgcDriverGetHsOffchipParam(uint16_t* first, uint16_t* second) { return 0; } + +int APS5_VABI sceAgcDriverSetValidationDisabled() { + return DEBUG_UNAVAILABLE; +} + +int APS5_VABI sceAgcDriverGetCaptureConfig() { + return DEBUG_UNAVAILABLE; +} + +int APS5_VABI sceAgcDriverSetCaptureConfig() { + return DEBUG_UNAVAILABLE; +} + +int APS5_VABI sceAgcDriverGetGpuPrintfWorkArea() { + return DRIVER_FEATURE_UNAVAILABLE; +} + +int APS5_VABI sceAgcDriverSetGpuPrintfEnabled() { + return DRIVER_FEATURE_UNAVAILABLE; +} + +int APS5_VABI sceAgcDriverDbgReadRegister() { + return 0; +} + +int APS5_VABI sceAgcDriverDbgWriteRegister() { + return DRIVER_FEATURE_UNAVAILABLE; +} + +int APS5_VABI sceAgcDriverGetGpuRefClks() { + return DRIVER_FEATURE_UNAVAILABLE; +} + +int APS5_VABI sceAgcDriverIsSuspendPointInFlight() { + return DRIVER_FEATURE_UNAVAILABLE; +} + +int APS5_VABI sceAgcDriverIsPaDebug() { + return 0; +} + +int APS5_VABI sceAgcDriverGetExceptionStatus() { + return 0; +} + +int APS5_VABI sceAgcDriverGetTraceInitiator() { + return 0; +} + +int APS5_VABI sceAgcDriverRegisterGpuResetCallbacks() { + return 0; +} + +int APS5_VABI sceAgcDriverUnregisterGpuResetCallbacks() { + return 0; +} + +int APS5_VABI sceAgdResetGpu() { + return GPU_RESET_UNAVAILABLE; +} + +int APS5_VABI sceAgcDriverSysSubmitFlipHandleProxy() { + return 0; +} + +int APS5_VABI sceAgcDriverUserDataImmediateWrite() { + return 0; +} + +int APS5_VABI sceAgcDriverUserDataImmediateWritePacket() { + return 0; +} + +int APS5_VABI sceAgcDriverGetCoredumpMode(uint32_t* mode) { + if (mode == nullptr) return DRIVER_NULL_OUTPUT; + *mode = 0; + return 0; +} + +int APS5_VABI sceAgcDriverAllocateToolMemoryForGpuReset(uint64_t, uint64_t, uint64_t, void* range) { + if (range != nullptr) std::memset(range, 0, TOOL_MEMORY_RANGE_SIZE); + return DRIVER_FEATURE_UNAVAILABLE; +} + +int APS5_VABI sceAgcDriverGetAllocatedToolMemoryForGpuReset(uint64_t, void* range) { + if (range != nullptr) std::memset(range, 0, TOOL_MEMORY_RANGE_SIZE); + return DRIVER_FEATURE_UNAVAILABLE; +} + +} +extern "C" { + +int APS5_VABI sceAgcDriverAcquireHDRScopesACQ() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverGetCoredumpAddress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverHp3dLockEnd() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverHp3dLockStart() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverIsEsvmProcess() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverNopDataWritePacket() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverReleaseHDRScopesACQ() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverSetTargetRingForDiag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverSetValueToSharedGds() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverSetWorkloadStreamInactive() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverSubmitReprojectionAcb() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverSuspendPointSubmitCdbg() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAgcDriverSysGetGfxAppGpuResetStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp index 61a1612f87..deb5bb8e8d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp @@ -1,14 +1,23 @@ #include "BdaShader.hpp" #include "ColorTransferTests.hpp" +#include "HostImportFailure.hpp" #include #include "prx/libSceAgcDriver/Execution/include/BdaFeatures.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include +#include #include +#include #include +#include #include #include +#include +#include +#include +#include +#include #include namespace AgcDriver::Graphics { @@ -21,9 +30,66 @@ namespace { using namespace AgcDriver::Graphics; +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; +} + +bool canSkipVulkanFailure(const char* requestedName, const char* requireVulkan) { + return (requestedName == nullptr || *requestedName == '\0') && requireVulkan == nullptr; +} + +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(TM) Graphics", VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU), + properties("NVIDIA GeForce RTX 5080", VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU), + }; + Require(!selectDevice({}, {}).has_value(), "an empty Vulkan device list selected a device"); + Require(selectDevice(devices, {}) == 1u, "default selection did not prefer the discrete GPU"); + Require(selectDevice(devices, "AMD") == 0u, "an AMD request lost to an unrelated discrete GPU"); + Require(selectDevice(devices, "aMd rAdEoN") == 0u, "GPU name matching was not case-insensitive"); + Require(selectDevice(devices, "RTX 5080") == 1u, "partial GPU name matching failed"); + Require(!selectDevice(devices, "missing device").has_value(), "an unmatched GPU request selected another device"); + Require(canSkipVulkanFailure(nullptr, nullptr), "an optional Vulkan test without a device request cannot skip"); + Require(canSkipVulkanFailure("", nullptr), "an empty GPU request prevents an optional Vulkan skip"); + Require(!canSkipVulkanFailure("missing device", nullptr), "an unmatched GPU request can be reported as a skip"); + Require(!canSkipVulkanFailure(nullptr, "1"), "a required Vulkan test can be reported as a skip"); + devices[0].apiVersion = VK_API_VERSION_1_0; + Require(!selectDevice(devices, "AMD").has_value(), "a GPU without Vulkan 1.1 matched the request"); + std::cout << "BDA device selection CPU tests passed\n"; +} + class Device { public: - Device() { + explicit Device(bool importHostMemory = false) { #ifdef _WIN32 library = SDL_LoadObject("vulkan-1.dll"); #else @@ -44,27 +110,29 @@ public: Require(count != 0, "no Vulkan device"); std::vector devices(count); Check(enumerate(instance, &count, devices.data()), "vkEnumeratePhysicalDevices"); - 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; - } - }; + devices.resize(count); const auto physicalProperties = function("vkGetPhysicalDeviceProperties"); - int selectedRank = -1; - for (const auto physical : devices) { - VkPhysicalDeviceProperties candidate{}; - physicalProperties(physical, &candidate); - if (candidate.apiVersion < VK_API_VERSION_1_1) continue; - const int rank = rankDeviceType(candidate.deviceType); - if (rank <= selectedRank) continue; - context.physical = physical; - selectedRank = rank; - cpu = candidate.deviceType == VK_PHYSICAL_DEVICE_TYPE_CPU; + std::vector deviceProperties(count); + for (std::size_t index = 0; index < devices.size(); ++index) { + physicalProperties(devices[index], &deviceProperties[index]); } - Require(context.physical != VK_NULL_HANDLE, "no Vulkan 1.1 device"); + const char* requestedName = std::getenv("ANYPS5_GPU"); + const std::string_view request = requestedName != nullptr ? requestedName : ""; + const auto selected = selectDevice(deviceProperties, request); + if (!selected.has_value()) { + if (!request.empty()) { + std::string candidateNames; + for (const auto& candidate : deviceProperties) { + if (!candidateNames.empty()) candidateNames += ", "; + candidateNames += candidate.deviceName; + } + throw std::runtime_error(std::string("no Vulkan 1.1 device whose name contains ANYPS5_GPU=\"") + std::string(request) + "\"; devices: " + candidateNames); + } + throw std::runtime_error("no Vulkan 1.1 device"); + } + context.physical = devices[*selected]; + cpu = deviceProperties[*selected].deviceType == VK_PHYSICAL_DEVICE_TYPE_CPU; + std::cout << "Using \"" << deviceProperties[*selected].deviceName << "\"\n"; const auto extensions = function("vkEnumerateDeviceExtensionProperties"); Check(extensions(context.physical, nullptr, &count, nullptr), "vkEnumerateDeviceExtensionProperties"); std::vector available(count); @@ -86,11 +154,13 @@ public: VkPhysicalDeviceFeatures enabled{}; enabled.shaderInt64 = VK_TRUE; address.pNext = &bytes; - const std::array extensionsEnabled{VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME, VK_KHR_8BIT_STORAGE_EXTENSION_NAME}; + std::vector extensionsEnabled{VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME, VK_KHR_8BIT_STORAGE_EXTENSION_NAME}; + const bool externalHost = importHostMemory && std::any_of(available.begin(), available.end(), [](const auto& item) { return std::strcmp(item.extensionName, VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME) == 0; }); + if (externalHost) extensionsEnabled.push_back(VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME); VkDeviceCreateInfo device{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, &address}; device.queueCreateInfoCount = 1; device.pQueueCreateInfos = &queue; - device.enabledExtensionCount = 2; + device.enabledExtensionCount = static_cast(extensionsEnabled.size()); device.ppEnabledExtensionNames = extensionsEnabled.data(); device.pEnabledFeatures = &enabled; Check(function("vkCreateDevice")(context.physical, &device, nullptr, &context.device), "vkCreateDevice"); @@ -99,6 +169,13 @@ public: VkPhysicalDeviceProperties properties{}; function("vkGetPhysicalDeviceProperties")(context.physical, &properties); context.limits = properties.limits; + if (externalHost) { + VkPhysicalDeviceExternalMemoryHostPropertiesEXT host{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_MEMORY_HOST_PROPERTIES_EXT}; + VkPhysicalDeviceProperties2 extended{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &host}; + function("vkGetPhysicalDeviceProperties2")(context.physical, &extended); + context.hostImportAlignment = host.minImportedHostPointerAlignment; + } + if (importHostMemory) std::printf("host import device %s vendor=0x%04x device=0x%04x driver=0x%08x alignment=%llu\n", properties.deviceName, properties.vendorID, properties.deviceID, properties.driverVersion, static_cast(context.hostImportAlignment)); context.bufferDeviceAddress = true; context.Function("vkGetDeviceQueue")(context.device, family, 0, &context.queue); VkCommandPoolCreateInfo pool{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; @@ -142,8 +219,11 @@ private: int main(int argc, char** argv) { try { + deviceSelectionTests(); + if (argc == 2 && std::string_view(argv[1]) == "--device-selection-cpu") return 0; RunBdaContractTests(); - if (argc == 2) { + const bool importFailures = argc == 2 && std::strcmp(argv[1], "--host-import-failures") == 0; + if (argc == 2 && !importFailures) { const auto shader = MakeBdaTestShader(0x7fff12340000ULL, 32); std::ofstream file(argv[1], std::ios::binary); file.write(reinterpret_cast(shader.data()), static_cast(shader.size() * sizeof(std::uint32_t))); @@ -152,12 +232,20 @@ int main(int argc, char** argv) { } std::unique_ptr device; try { - device = std::make_unique(); + device = std::make_unique(importFailures); } catch (const std::exception& error) { - if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr) throw; + if (!canSkipVulkanFailure(std::getenv("ANYPS5_GPU"), std::getenv("ANYPS5_REQUIRE_VULKAN"))) throw; std::cout << "skipped, no usable Vulkan device: " << error.what() << '\n'; return 77; } + if (importFailures) { + if (device->GetContext().hostImportAlignment == 0) { + std::cout << "skipped, no external host memory import support\n"; + return 77; + } + AgcDriver::Tests::RunHostImportFailureTests(device->GetContext(), true); + return 0; + } Buffer buffer(device->GetContext(), 256, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT); Require(buffer.DeviceAddress() != 0 && buffer.Bytes().size() == 256, "invalid real BDA buffer"); buffer.Bytes()[255] = std::byte{0x5a}; diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp index 7f9b8b2eca..098d311465 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp @@ -4,6 +4,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include #include +#include #include #include #include @@ -15,6 +16,12 @@ namespace { +std::string Hex(std::uint64_t value) { + char text[24]; + std::snprintf(text, sizeof(text), "0x%llx", static_cast(value)); + return text; +} + using namespace AgcDriver::Graphics; class Pipeline { @@ -293,7 +300,7 @@ static void RunBdaAnchorTests(const Context& context) { const auto expected = run(programs.plain, index, anchor, batch); const auto actual = run(programs.anchored, index, anchor, batch); const bool faultsMatch = programs.reads.lanes == 1u ? std::memcmp(&expected.fault, &actual.fault, sizeof(expected.fault)) == 0 : expected.fault.state == actual.fault.state; - Require(faultsMatch && expected.values == actual.values, "an anchored BDA read differs from the same read through the lookups at anchor 0x" + std::to_string(anchor) + " + " + std::to_string(batch[0]) + " + (" + std::to_string(read.immediate) + "), " + std::to_string(read.bits) + " bits, " + std::to_string(read.dwords) + " dwords"); + Require(faultsMatch && expected.values == actual.values, "an anchored BDA read differs from the same read through the lookups at anchor " + Hex(anchor) + " + " + std::to_string(batch[0]) + " + (" + std::to_string(read.immediate) + "), " + std::to_string(read.bits) + " bits, " + std::to_string(read.dwords) + " dwords"); } } } @@ -307,6 +314,9 @@ static void RunBdaAnchorTests(const Context& context) { for (std::uint32_t lane = 0; lane < 32u; ++lane) lows.push_back(lane * 0x24u); std::vector strided; for (std::uint32_t lane = 0; lane < lanes; ++lane) strided.push_back(lane * 16u); + auto halfword = build({{{-1, 16, 1, 1}}, false, false, false, lanes}); + compare(*halfword, {guest + 0x100}, {1u}); + compare(*halfword, {guest + 0x100}, {0u, 1u}); auto continuing = build({reads, false, false, false, lanes}); compare(*continuing, anchors, lows); compare(*continuing, {guest + 0x400}, strided); @@ -326,11 +336,11 @@ static void RunBdaAnchorTests(const Context& context) { }; const auto loads = [&](std::uint32_t read, std::uint64_t anchor, std::uint32_t low, std::initializer_list values) { const auto outcome = run(*direct, read, anchor, {low}); - Require(outcome.fault.state == Abi::FaultState::Empty && std::equal(values.begin(), values.end(), outcome.values.begin()), "a read the anchor holds did not load directly at anchor 0x" + std::to_string(anchor) + " + " + std::to_string(low) + ", read " + std::to_string(read)); + Require(outcome.fault.state == Abi::FaultState::Empty && std::equal(values.begin(), values.end(), outcome.values.begin()), "a read the anchor holds did not load directly at anchor " + Hex(anchor) + " + " + std::to_string(low) + ", read " + std::to_string(read)); }; const auto misses = [&](std::uint32_t read, std::uint64_t anchor, std::uint32_t low) { const auto outcome = run(*direct, read, anchor, {low}); - Require(outcome.fault.state == Abi::FaultState::Empty && outcome.values[0] == BdaAnchoredReadSentinel, "a read the anchor does not hold did not take the lookup path at anchor 0x" + std::to_string(anchor) + " + " + std::to_string(low) + ", read " + std::to_string(read)); + Require(outcome.fault.state == Abi::FaultState::Empty && outcome.values[0] == BdaAnchoredReadSentinel, "a read the anchor does not hold did not take the lookup path at anchor " + Hex(anchor) + " + " + std::to_string(low) + ", read " + std::to_string(read)); }; loads(2, guest + 0x100, 0, {dword(words, 0)}); loads(2, guest + 0x100, 0xc, {dword(words, 0xc)}); @@ -511,6 +521,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); } @@ -544,9 +568,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 d8fffa7de3..ceacffde72 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -29,6 +29,7 @@ #ifndef _WIN32 #include +#include #endif namespace { @@ -760,6 +761,8 @@ public: ImportAllocationProbe& operator=(const ImportAllocationProbe&) = delete; std::vector calls; + VkResult failure = VK_SUCCESS; + std::function onImport; private: static PFN_vkVoidFunction VKAPI_CALL Resolve(VkDevice device, const char* name) { @@ -773,6 +776,11 @@ private: if (next->sType != VK_STRUCTURE_TYPE_IMPORT_MEMORY_HOST_POINTER_INFO_EXT) continue; const auto* imported = reinterpret_cast(next); active->calls.push_back({imported->pHostPointer, info->allocationSize}); + if (active->onImport) { + auto callback = std::move(active->onImport); + callback(); + } + if (active->failure != VK_SUCCESS) return active->failure; } const auto allocate = reinterpret_cast(active->original(device, "vkAllocateMemory")); Require(allocate != nullptr, "import allocation probe has no original allocator"); @@ -799,6 +807,44 @@ private: std::array storage{}; }; +void importMappedFailureGenerationTests(const Context& context) { + ClearHostImports(context.device); + WindowedRange range(context, WindowedRange::piece, 1); + ImportAllocationProbe probe(range.importing); + probe.failure = VK_ERROR_INVALID_EXTERNAL_HANDLE; + GuestAllocations::Mapped mapped; + const auto collect = [&] { + mapped.clear(); + const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + for (const auto& owner : lease) if (owner->address == range.First()) mapped.push_back(owner); + Require(mapped.size() == 1u, "the map failure fixture lost its registered range"); + }; + collect(); + std::unique_ptr unrelated; + probe.onImport = [&] { unrelated = std::make_unique(); }; + const auto generation = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + Require(ImportMappedRanges(range.importing, mapped, generation, true), "the first failed map import was deferred"); + Require(probe.calls.size() == 1u && unrelated != nullptr, "the map failure fixture did not mutate an unrelated range during allocation"); + Require(GuestAllocations::GuestAllocationsGeneration_nid_postfix() != generation, "the concurrent unrelated mapping did not advance the registry"); + Require(ImportMappedRanges(range.importing, mapped, GuestAllocations::GuestAllocationsGeneration_nid_postfix(), true), "the repeated failed map import was deferred"); + Require(probe.calls.size() == 1u, "an unrelated mapping during a failed import discarded its owner failure memo"); + } + { + GuestAllocations::Mutation mutation; + mutation.Remove(reinterpret_cast(range.First())); + mutation.Add(reinterpret_cast(range.First()), range.bytes, true, false, true); + } + collect(); + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + Require(ImportMappedRanges(range.importing, mapped, GuestAllocations::GuestAllocationsGeneration_nid_postfix(), true), "the replacement owner map import was deferred"); + Require(probe.calls.size() == 2u, "a replacement owner did not retry its predecessor's failed import"); + } + ClearHostImports(context.device); +} + void importStaleProbeTests(const Context& context, const BdaTestAccess& access) { for (const auto scenario : {0u, 1u, 2u}) { const bool held = scenario == 1u; @@ -976,6 +1022,807 @@ void importAtMapOverBudgetTests(const Context& context) { for (auto* block : {kept, used, mapped}) ::operator delete(block, std::align_val_t{bytes}); } +void importRequesterTests(const Context& context, const BdaTestAccess& access) { + constexpr std::size_t piece = 65536; + constexpr std::size_t pieces = 8; + auto* block = static_cast(::operator new(pieces * piece, std::align_val_t{piece})); + std::memset(block, 0x2d, pieces * piece); + auto importing = context; + importing.hostImportAlignment = piece; + SetHostImportWindow(0); + const auto address = [&](std::size_t index) { return reinterpret_cast(block + index * piece); }; + const auto change = [&](std::size_t index, bool add) { + GuestAllocations::Mutation mutation; + if (add) mutation.Add(block + index * piece, piece, true, index % 2 == 0, true); + else mutation.Remove(block + index * piece); + }; + const auto counted = [](ImportRequester requester) { return HostImportCounters().requesters[static_cast(requester)]; }; + const auto made = [&](ImportRequester requester) { return counted(requester).made; }; + Require(std::string(ImportRequesterName(ImportRequester::Map)) == "map" && std::string(ImportRequesterName(ImportRequester::Build)) == "build" && std::string(ImportRequesterName(ImportRequester::Access)) == "access" && std::string(ImportRequesterName(ImportRequester::Join)) == "join" && std::string(ImportRequesterName(ImportRequester::Segment)) == "segment", "an import requester has the wrong name"); + change(0, true); + { + GuestAllocations::Mapped ranges; + const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + for (const auto& range : lease) { + if (range->address == address(0)) ranges.push_back(range); + } + Require(ranges.size() == 1, "the requester test's mapped range is not registered"); + const auto before = made(ImportRequester::Map); + static_cast(ImportMappedRanges(importing, ranges, GuestAllocations::GuestAllocationsGeneration_nid_postfix(), true)); + Require(made(ImportRequester::Map) == before + 1 && HostImportCovers(importing, address(0), piece), "a range imported at map time was not counted for the map requester"); + } + change(1, true); + { + const auto before = made(ImportRequester::Access); + Require(HostImportFor(importing, address(1), 16) != nullptr && made(ImportRequester::Access) == before + 1, "a lookup's import was not counted for the access requester"); + } + change(2, true); + { + const auto before = made(ImportRequester::Segment); + const auto access = made(ImportRequester::Access); + Require(HostImportFor(importing, address(2), 16, ImportRequester::Segment) != nullptr && made(ImportRequester::Segment) == before + 1 && made(ImportRequester::Access) == access, "a write-back segment's import was not counted for the segment requester"); + } + change(3, true); + { + const auto before = counted(ImportRequester::Build); + const auto access = made(ImportRequester::Access); + GuestBufferMemory leased(importing); + leased.AcquireRegistered(); + leased.Upload(true); + leased.WriteBack(); + Require(made(ImportRequester::Build) > before.made && HostImportCovers(importing, address(3), piece) && made(ImportRequester::Access) == access, "an address-based build's import was not counted for the build requester"); + } + { + const auto live = HostImportLiveBytes(); + SetHostImportBudget(live); + change(4, true); + const auto before = counted(ImportRequester::Build); + { + GuestBufferMemory leased(importing); + leased.AcquireRegistered(); + leased.Upload(true); + leased.WriteBack(); + } + const auto after = counted(ImportRequester::Build); + SetHostImportBudget(0); + Require(after.refused > before.refused && after.refusedBytes >= before.refusedBytes + piece && after.made == before.made, "an address-based build's refusal was not counted for the build requester"); + } + change(5, true); + { + const auto before = counted(ImportRequester::Access); + access.limitMemory(0); + const auto* failed = HostImportFor(importing, address(5), 16); + access.limitMemory(std::nullopt); + Require(failed == nullptr && counted(ImportRequester::Access).failed == before.failed + 1, "a lookup's failed import was not counted for the access requester"); + } + { + constexpr std::size_t window = 2 * piece; + constexpr std::size_t large = 3 * piece; + auto* joined = static_cast(::operator new(2 * large, std::align_val_t{window})); + std::memset(joined, 0x1d, 2 * large); + SetHostImportWindow(window); + { + GuestAllocations::Mutation mutation; + mutation.Add(joined, large, true, false, true); + mutation.Add(joined + large, large, true, false, true); + } + const auto before = made(ImportRequester::Join); + const auto second = reinterpret_cast(joined + large); + { + GuestBufferMemory leased(importing); + leased.AcquireRegistered(); + leased.AddReadable(second - 16, 32); + leased.Upload(true); + leased.WriteBack(); + } + Require(made(ImportRequester::Join) == before + 1, "a join at bind was not counted for the join requester"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(joined); + mutation.Remove(joined + large); + } + SetHostImportWindow(0); + ::operator delete(joined, std::align_val_t{window}); + } + const auto counts = HostImportCounters(); + Require(counts.live >= 4 && counts.liveBytes == HostImportLiveBytes(), "the live import counts disagree with the live bytes"); + for (std::size_t index = 0; index < 6; ++index) change(index, false); + ClearHostImports(importing.device); + ::operator delete(block, std::align_val_t{piece}); +} + +void hostImportReportTests() { + HostImportCounts counts; + counts.live = 3; + counts.liveBytes = 3u << 20u; + counts.requesters[static_cast(ImportRequester::Map)] = {2, 1u << 20u, 0, 0, 0}; + counts.requesters[static_cast(ImportRequester::Build)] = {5, 4u << 20u, 7, 7u << 19u, 1, 4}; + counts.requesters[static_cast(ImportRequester::Segment)] = {1, 1u << 16u, 0, 0, 2}; + const auto text = FormatHostImportCounts(counts); + Require(text == "3 live (3.0 MiB); made 8 (5.1 MiB), refused 7 (3.5 MiB, 4 remembered), failed 3; by requester (made/refused/failed): map 2/0/0, build 5/7/1, access 0/0/0, join 0/0/0, segment 1/0/2", "the host import report is wrong: " + text); +} + +class PieceWindow { +public: + static constexpr std::size_t Piece = 65536; + static constexpr std::size_t Window = std::size_t{8} << 20u; + + explicit PieceWindow(const Context& context, bool views = false) : importing(context) { +#ifndef _WIN32 + if (views) { + reserved = mmap(nullptr, 2 * Window, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(reserved != MAP_FAILED, "cannot reserve the block of 16 KiB views"); + block = reinterpret_cast((reinterpret_cast(reserved) + Window - 1) / Window * Window); + file = memfd_create("anyps5-import-block-views", MFD_CLOEXEC); + Require(file >= 0 && ftruncate(file, static_cast(Window)) == 0, "cannot create the file behind the 16 KiB views"); + constexpr std::size_t view = 16384; + constexpr std::size_t count = Window / view; + for (std::size_t index = 0; index < count; ++index) { + const auto offset = static_cast((index % 4 * (count / 4) + index / 4) * view); + Require(mmap(block + index * view, view, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, file, offset) == block + index * view, "cannot map a 16 KiB view"); + } + } +#endif + if (block == nullptr) block = static_cast(::operator new(Window, std::align_val_t{Window})); + std::memset(block, 0x4b, Window); + importing.hostImportAlignment = Piece; + SetHostImportWindow(Window); + ClearHostImports(importing.device); + } + ~PieceWindow() { + for (const auto offset : registered) { + GuestAllocations::Mutation mutation; + mutation.Remove(block + offset); + } + ClearHostImports(importing.device); + SetHostImportBudget(0); + SetHostImportWindow(0); +#ifndef _WIN32 + if (reserved != nullptr) { + munmap(reserved, 2 * Window); + close(file); + return; + } +#endif + ::operator delete(block, std::align_val_t{Window}); + } + PieceWindow(const PieceWindow&) = delete; + PieceWindow& operator=(const PieceWindow&) = delete; + + void Add(std::size_t offset, std::size_t bytes, bool writable = true, bool gpu = true) { + GuestAllocations::Mutation mutation; + mutation.Add(block + offset, bytes, true, writable, gpu); + registered.push_back(offset); + } + void AddPieces(std::size_t from, std::size_t to) { + for (auto offset = from; offset < to; offset += Piece) Add(offset, Piece); + } + void Remove(std::size_t offset) { + GuestAllocations::Mutation mutation; + mutation.Remove(block + offset); + registered.erase(std::find(registered.begin(), registered.end(), offset)); + } + std::uintptr_t Address(std::size_t offset) const { return reinterpret_cast(block + offset); } + void Build() const { + GuestBufferMemory leased(importing); + leased.AcquireRegistered(); + leased.Upload(true); + leased.WriteBack(); + } + std::uint64_t Serial(std::size_t offset) const { return HostImportSerial(importing, Address(offset), Piece, false); } + std::size_t Imports(std::size_t from, std::size_t to) const { + std::vector serials; + for (auto offset = from; offset < to; offset += Piece) { + if (const auto serial = Serial(offset); serial != 0) serials.push_back(serial); + } + std::sort(serials.begin(), serials.end()); + return static_cast(std::unique(serials.begin(), serials.end()) - serials.begin()); + } + bool Covered(std::size_t from, std::size_t to) const { + for (auto offset = from; offset < to; offset += Piece) { + if (Serial(offset) == 0) return false; + } + return true; + } + bool Block(std::size_t offset, std::size_t bytes) const { return HostImportCovers(importing, Address(offset), bytes) && Serial(offset) == Serial(offset + bytes - Piece) && (offset == 0 || Serial(offset - Piece) != Serial(offset)) && (offset + bytes == Window || Serial(offset + bytes) != Serial(offset)); } + + Context importing; + +private: + std::uint8_t* block = nullptr; + void* reserved = nullptr; + int file = -1; + std::vector registered; +}; + +std::string blockText(const PieceWindow& window, std::size_t from, std::size_t to) { + return std::to_string(window.Imports(from, to)) + " imports"; +} + +void importBlockMapTests(const Context& context, bool views) { + PieceWindow window(context, views); + constexpr auto half = PieceWindow::Window / 2; + for (std::size_t offset = 0; offset < half; offset += PieceWindow::Piece) { + window.Add(offset, PieceWindow::Piece); + GuestAllocations::Mapped ranges; + const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + for (const auto& range : lease) { + if (range->address == window.Address(offset)) ranges.push_back(range); + } + static_cast(ImportMappedRanges(window.importing, ranges, GuestAllocations::GuestAllocationsGeneration_nid_postfix(), true)); + } + Require(HostImportFor(window.importing, window.Address(0), 16) != nullptr, "the first of 64 pieces mapped one by one was not imported"); + Require(window.Imports(0, half) == 1 && window.Block(0, half), std::string(views ? "64 adjacent 64 KiB pieces of four 16 KiB views each" : "64 adjacent 64 KiB pieces") + " mapped one by one were not imported as one 4 MiB block: " + blockText(window, 0, half)); +} + +void importBlockBuildTests(const Context& context) { + PieceWindow window(context); + constexpr auto half = PieceWindow::Window / 2; + window.AddPieces(half, PieceWindow::Window); + window.Build(); + Require(window.Imports(half, PieceWindow::Window) == 1 && window.Block(half, half), "64 adjacent 64 KiB pieces were not imported as one 4 MiB block by an address-based build: " + blockText(window, half, PieceWindow::Window)); + window.Add(0, half, false); + window.Build(); + Require(window.Imports(0, PieceWindow::Window) == 2 && window.Block(0, half) && window.Block(half, half), "a 4 MiB read-only range and 64 writable 64 KiB pieces in one window were not imported as the range and one 4 MiB block: " + blockText(window, 0, PieceWindow::Window)); +} + +void importBlockHoleTests(const Context& context) { + PieceWindow window(context); + constexpr auto half = PieceWindow::Window / 2; + window.Add(0, half); + window.AddPieces(half, PieceWindow::Window - PieceWindow::Piece); + window.Build(); + constexpr std::size_t KiB = 1024; + Require(window.Imports(0, PieceWindow::Window) == 7, "a window whose last 64 KiB is unmapped was not imported as 7 blocks: " + blockText(window, 0, PieceWindow::Window)); + Require(window.Block(0, half) && window.Block(half, 2048 * KiB) && window.Block(half + 2048 * KiB, 1024 * KiB) && window.Block(half + 3072 * KiB, 512 * KiB) && window.Block(half + 3584 * KiB, 256 * KiB) && window.Block(half + 3840 * KiB, 128 * KiB) && window.Block(half + 3968 * KiB, 64 * KiB), "a window whose last 64 KiB is unmapped was not split into the largest aligned blocks"); +} + +void importBlockUnalignedTests(const Context& context) { + PieceWindow window(context); + constexpr std::size_t KiB = 1024; + window.AddPieces(768 * KiB, 3840 * KiB); + window.Build(); + Require(window.Imports(768 * KiB, 3840 * KiB) == 5, "48 pieces from 768 KiB were not imported as 5 aligned blocks: " + blockText(window, 768 * KiB, 3840 * KiB)); + Require(window.Block(768 * KiB, 256 * KiB) && window.Block(1024 * KiB, 1024 * KiB) && window.Block(2048 * KiB, 1024 * KiB) && window.Block(3072 * KiB, 512 * KiB) && window.Block(3584 * KiB, 256 * KiB), "48 pieces from 768 KiB were not split into 256 KiB, 1 MiB, 1 MiB, 512 KiB and 256 KiB blocks"); +} + +void importBlockFlagTests(const Context& context) { + PieceWindow window(context); + constexpr auto half = PieceWindow::Window / 2; + constexpr auto split = half / 2; + window.AddPieces(0, split); + window.Add(split, PieceWindow::Piece, true, false); + window.AddPieces(split + PieceWindow::Piece, half); + window.Build(); + Require(window.Serial(split) == 0 && window.Imports(0, split) == 1 && window.Block(0, split), "pieces before a range that is not GPU mapped were not imported as one block: " + blockText(window, 0, split)); + Require(window.Imports(split + PieceWindow::Piece, half) == 5 && window.Covered(split + PieceWindow::Piece, half), "pieces after a range that is not GPU mapped were not imported as the 5 aligned blocks after it: " + blockText(window, split + PieceWindow::Piece, half)); +} + +void importBlockRemapTests(const Context& context) { + PieceWindow window(context); + constexpr auto half = PieceWindow::Window / 2; + window.AddPieces(0, half); + window.Build(); + Require(window.Imports(0, half) == 1 && window.Block(0, half), "64 pieces were not imported as one block before the remap: " + blockText(window, 0, half)); + const auto before = window.Serial(0); + constexpr auto remapped = 10 * PieceWindow::Piece; + window.Remove(remapped); + window.Add(remapped, PieceWindow::Piece); + window.Build(); + Require(window.Imports(0, half) == 1 && window.Block(0, half) && window.Serial(0) != before, "a piece remapped inside a block did not retire the block and import it again whole: " + blockText(window, 0, half)); +} + +void importBlockFailureTests(const Context& context, const BdaTestAccess& access) { + PieceWindow window(context); + constexpr std::size_t MiB = std::size_t{1} << 20u; + constexpr auto half = PieceWindow::Window / 2; + window.AddPieces(0, PieceWindow::Window); + access.limitImports(half); + window.Build(); + Require(window.Imports(0, PieceWindow::Window) == 2 && window.Block(0, half) && window.Block(half, half), "a window whose 8 MiB import failed did not step down to two 4 MiB blocks: " + blockText(window, 0, PieceWindow::Window)); + const auto attempts = access.allocationAttempts(); + window.Build(); + Require(access.allocationAttempts() == attempts && window.Imports(0, PieceWindow::Window) == 2, "an unchanged failed block was tried again"); + access.limitImports(std::nullopt); + constexpr auto remapped = 7 * MiB; + window.Remove(remapped); + window.Add(remapped, PieceWindow::Piece); + window.Build(); + Require(window.Imports(0, PieceWindow::Window) == 1 && window.Block(0, PieceWindow::Window), "a block that failed was not tried again after one of its ranges was mapped again: " + blockText(window, 0, PieceWindow::Window)); +} + +void importBlockBudgetTests(const Context& context) { + PieceWindow window(context); + constexpr std::size_t MiB = std::size_t{1} << 20u; + constexpr auto half = PieceWindow::Window / 2; + window.AddPieces(0, half); + SetHostImportBudget(HostImportLiveBytes() + 3 * MiB); + window.Build(); + Require(window.Imports(0, half) == 2 && window.Block(0, 2 * MiB) && window.Block(2 * MiB, MiB) && !window.Covered(3 * MiB, half), "a 4 MiB block over the budget did not step down to a 2 MiB and a 1 MiB block: " + blockText(window, 0, half)); + const auto low = window.Serial(0); + const auto high = window.Serial(2 * MiB); + window.Add(6 * MiB, PieceWindow::Piece); + window.Build(); + Require(window.Serial(0) == low && window.Serial(2 * MiB) == high && window.Imports(0, PieceWindow::Window) == 2, "blocks stepped down for the budget were imported again after an unrelated range was mapped: " + blockText(window, 0, PieceWindow::Window)); +} + +void importRefusalMemoryTests(const Context& context) { + constexpr std::size_t piece = 4096; + constexpr std::size_t count = 12000; + constexpr std::size_t fitting = count * 6 / 10; + constexpr std::size_t refused = count - fitting; + auto importing = context; + importing.hostImportAlignment = piece; + auto* block = static_cast(::operator new(2 * count * piece, std::align_val_t{piece})); + auto* other = static_cast(::operator new(piece, std::align_val_t{piece})); + const auto at = [&](std::size_t index) { return block + 2 * index * piece; }; + SetHostImportWindow(std::size_t{8} << 20u); + ClearHostImports(importing.device); + { + GuestAllocations::Mutation mutation; + for (std::size_t index = 0; index < count; ++index) mutation.Add(at(index), piece, true, true, true); + } + const auto budget = HostImportLiveBytes() + fitting * piece; + SetHostImportBudget(budget); + bool added = false; + const auto rebuild = [&] { + { + GuestAllocations::Mutation mutation; + if (added) mutation.Remove(other); + else mutation.Add(other, piece, true, true, false); + } + added = !added; + GuestBufferMemory leased(importing); + leased.AcquireRegistered(); + }; + const auto counted = [](ImportRequester requester) { return HostImportCounters().requesters[static_cast(requester)]; }; + const auto tries = [](const HostImportRequesterCounts& requested) { return requested.refused - requested.remembered; }; + auto before = counted(ImportRequester::Build); + rebuild(); + auto after = counted(ImportRequester::Build); + Require(after.made - before.made == fitting && tries(after) - tries(before) == refused, "a registry of 12000 ranges over a budget for 7200 of them was not imported and refused as expected: " + std::to_string(after.made - before.made) + " made, " + std::to_string(tries(after) - tries(before)) + " refused"); + for (int round = 1; round <= 4; ++round) { + before = after; + rebuild(); + after = counted(ImportRequester::Build); + Require(tries(after) == tries(before), "rebuild " + std::to_string(round) + " tried " + std::to_string(tries(after) - tries(before)) + " refused ranges again with the budget unchanged"); + Require(after.refused - before.refused == refused && after.remembered - before.remembered == refused && after.made == before.made, "rebuild " + std::to_string(round) + " did not count its remembered refusals as refusals"); + } + { + GuestAllocations::Mutation mutation; + mutation.Remove(at(0)); + } + before = after; + rebuild(); + after = counted(ImportRequester::Build); + Require(after.made - before.made == 1 && HostImportCovers(importing, reinterpret_cast(at(fitting)), piece), "the budget a retired import freed was not used for a remembered refusal: " + std::to_string(after.made - before.made) + " made"); + SetHostImportBudget(budget + piece); + before = after; + rebuild(); + after = counted(ImportRequester::Build); + Require(after.made - before.made == 1 && HostImportCovers(importing, reinterpret_cast(at(fitting + 1)), piece), "a larger budget did not end the remembered refusals: " + std::to_string(after.made - before.made) + " made"); + const auto lookups = counted(ImportRequester::Access); + static_cast(HostImportFor(importing, reinterpret_cast(at(count - 1)), 16)); + const auto looked = counted(ImportRequester::Access); + Require(looked.remembered == lookups.remembered && looked.made + looked.refused + looked.failed == lookups.made + lookups.refused + lookups.failed + 1, "a lookup that may evict was answered from the build's remembered refusals"); + { + GuestAllocations::Mutation mutation; + for (std::size_t index = 1; index < count; ++index) mutation.Remove(at(index)); + if (added) mutation.Remove(other); + } + ClearHostImports(importing.device); + SetHostImportBudget(0); + SetHostImportWindow(0); + ::operator delete(other, std::align_val_t{piece}); + ::operator delete(block, std::align_val_t{piece}); +} + +void evictingRefusalMemoryTests(const Context& context) { + constexpr std::size_t piece = 65536; + constexpr std::size_t filled = 32; + constexpr std::size_t count = filled + 3; + auto importing = context; + importing.hostImportAlignment = piece; + auto* block = static_cast(::operator new(2 * count * piece, std::align_val_t{piece})); + const auto at = [&](std::size_t index) { return block + 2 * index * piece; }; + const auto address = [&](std::size_t index) { return reinterpret_cast(at(index)); }; + ClearHostImports(importing.device); + { + GuestAllocations::Mutation mutation; + for (std::size_t index = 0; index < count; ++index) mutation.Add(at(index), piece, true, true, true); + } + const auto budget = HostImportLiveBytes() + filled * piece; + SetHostImportBudget(budget); + for (std::size_t index = 0; index < filled; ++index) Require(HostImportFor(importing, address(index), 16) != nullptr, "a piece inside the import budget was not imported"); + Require(HostImportLiveBytes() == budget, "the pieces did not fill the import budget"); + const auto counted = [] { return HostImportCounters().requesters[static_cast(ImportRequester::Access)]; }; + auto before = counted(); + for (int request = 0; request < 8; ++request) Require(HostImportFor(importing, address(filled), 16) == nullptr, "a piece over a full budget with nothing evictable was imported"); + auto after = counted(); + Require(after.refused - before.refused == 8 && after.remembered - before.remembered == 7, "8 evicting requests for one refused piece at a full budget with nothing evictable scanned the imports " + std::to_string(8 - (after.remembered - before.remembered)) + " times, not once"); + SetHostImportBudget(budget - piece); + before = counted(); + Require(HostImportFor(importing, address(filled), 16) == nullptr && counted().remembered == before.remembered + 1, "a remembered evicting refusal ended when the budget was lowered"); + SetHostImportBudget(budget); + { + GuestAllocations::Mutation mutation; + mutation.Remove(at(0)); + } + before = counted(); + Require(HostImportFor(importing, address(filled), 16) != nullptr && counted().remembered == before.remembered, "an evicting request was answered from its remembered refusal after an import retired"); + static_cast(HostImportFor(importing, address(filled + 1), 16)); + before = counted(); + Require(HostImportFor(importing, address(filled + 1), 16) == nullptr && counted().remembered == before.remembered + 1, "a second evicting request for a refused piece was not answered from memory"); + SetHostImportBudget(budget + piece); + before = counted(); + Require(HostImportFor(importing, address(filled + 1), 16) != nullptr && counted().remembered == before.remembered, "an evicting request was answered from its remembered refusal after the budget was raised"); + static_cast(HostImportFor(importing, address(filled + 2), 16)); + { + GuestBufferMemory upload(importing); + upload.Upload(false); + upload.WriteBack(); + } + before = counted(); + Require(HostImportFor(importing, address(filled + 2), 16) == nullptr && counted().remembered == before.remembered + 1, "a remembered evicting refusal ended when a build raised the floor while no import could age out of its protection yet"); + for (int use = 0; use < 200; ++use) Require(HostImportFor(importing, address(filled + 1), 16) != nullptr, "a live import was not found"); + before = counted(); + Require(HostImportFor(importing, address(filled + 2), 16) != nullptr && counted().remembered == before.remembered, "an evicting request was answered from its remembered refusal after the imports aged out of their protection"); + { + GuestAllocations::Mutation mutation; + for (std::size_t index = 1; index < count; ++index) mutation.Remove(at(index)); + } + ClearHostImports(importing.device); + SetHostImportBudget(0); + ::operator delete(block, std::align_val_t{piece}); +} + +void evictingRefusalAgingTests(const Context& context) { + constexpr std::size_t piece = 65536; + constexpr std::size_t filled = 16; + auto importing = context; + importing.hostImportAlignment = piece; + auto* block = static_cast(::operator new(2 * (filled + 1) * piece, std::align_val_t{piece})); + const auto at = [&](std::size_t index) { return block + 2 * index * piece; }; + const auto address = [&](std::size_t index) { return reinterpret_cast(at(index)); }; + ClearHostImports(importing.device); + { + GuestAllocations::Mutation mutation; + for (std::size_t index = 0; index <= filled; ++index) mutation.Add(at(index), piece, true, true, true); + } + SetHostImportBudget(HostImportLiveBytes() + filled * piece); + for (std::size_t index = 0; index < filled; ++index) Require(HostImportFor(importing, address(index), 16) != nullptr, "a piece inside the import budget was not imported"); + { + GuestBufferMemory upload(importing); + upload.Upload(false); + upload.WriteBack(); + } + const auto counted = [] { return HostImportCounters().requesters[static_cast(ImportRequester::Access)]; }; + static_cast(HostImportFor(importing, address(filled), 16)); + auto before = counted(); + Require(HostImportFor(importing, address(filled), 16) == nullptr && counted().remembered == before.remembered + 1, "a refusal while the imports were recent but no longer held by a build was not remembered"); + for (int use = 0; use < 200; ++use) Require(HostImportFor(importing, address(filled - 1), 16) != nullptr, "a live import was not found"); + before = counted(); + Require(HostImportFor(importing, address(filled), 16) != nullptr && counted().remembered == before.remembered, "an evicting request was answered from its remembered refusal after imports no build held aged out of their protection"); + { + GuestAllocations::Mutation mutation; + for (std::size_t index = 0; index <= filled; ++index) mutation.Remove(at(index)); + } + ClearHostImports(importing.device); + SetHostImportBudget(0); + ::operator delete(block, std::align_val_t{piece}); +} + +void evictingRefusalRangeTests(const Context& context) { + PieceWindow window(context); + constexpr auto piece = PieceWindow::Piece; + constexpr auto fillers = PieceWindow::Window / 2; + window.Add(0, 2 * piece); + window.Add(2 * piece, piece); + window.Add(3 * piece, piece); + for (std::size_t filler = 0; filler < 4; ++filler) { + window.Add(fillers + 2 * filler * piece, piece); + Require(HostImportFor(window.importing, window.Address(fillers + 2 * filler * piece), 16) != nullptr, "a filler piece inside the import budget was not imported"); + } + SetHostImportBudget(HostImportLiveBytes() + piece); + Require(HostImportFor(window.importing, window.Address(0), 16) == nullptr, "a 128 KiB range in a 256 KiB block over the budget was imported"); + Require(HostImportFor(window.importing, window.Address(2 * piece), 16) != nullptr && window.Block(2 * piece, piece), "a 64 KiB range of the same 256 KiB block was answered from the 128 KiB range's refusal instead of stepping down to its own import"); +} + +void evictingRefusalBlockRangesTests(const Context& context) { + PieceWindow window(context); + constexpr auto piece = PieceWindow::Piece; + constexpr auto fillers = PieceWindow::Window / 2; + window.AddPieces(0, 4 * piece); + for (std::size_t filler = 0; filler < 4; ++filler) { + window.Add(fillers + 2 * filler * piece, piece); + Require(HostImportFor(window.importing, window.Address(fillers + 2 * filler * piece), 16) != nullptr, "a filler piece inside the import budget was not imported"); + } + SetHostImportBudget(HostImportLiveBytes()); + const auto counted = [] { return HostImportCounters().requesters[static_cast(ImportRequester::Access)]; }; + const auto before = counted(); + for (int request = 0; request < 8; ++request) Require(HostImportFor(window.importing, window.Address(static_cast(request % 2) * piece), 16) == nullptr, "a 64 KiB range of a block over a full budget with nothing evictable was imported"); + const auto after = counted(); + Require(after.refused - before.refused == 8 && after.remembered - before.remembered == 7, "8 evicting requests alternating between two 64 KiB ranges of one refused 256 KiB block scanned the imports " + std::to_string(8 - (after.remembered - before.remembered)) + " times, not once"); + SetHostImportBudget(HostImportLiveBytes() + piece); + Require(HostImportFor(window.importing, window.Address(piece), 16) != nullptr && window.Block(piece, piece), "a 64 KiB range of the block was answered from the full budget's refusal instead of stepping down to its own import once the budget had room for it"); +} + +void evictingRefusalJoinTests(const Context& context) { + PieceWindow window(context); + constexpr auto piece = PieceWindow::Piece; + constexpr auto fillers = PieceWindow::Window / 2; + window.AddPieces(0, 3 * piece); + for (std::size_t filler = 0; filler < 4; ++filler) { + window.Add(fillers + 2 * filler * piece, piece); + Require(HostImportFor(window.importing, window.Address(fillers + 2 * filler * piece), 16) != nullptr, "a filler piece inside the import budget was not imported"); + } + SetHostImportBudget(HostImportLiveBytes()); + const auto upload = [&] { + GuestBufferMemory plain(window.importing); + plain.AddReadable(window.Address(0), 3 * piece); + plain.Upload(false); + plain.WriteBack(); + }; + const auto counted = [] { return HostImportCounters().requesters[static_cast(ImportRequester::Join)]; }; + const auto before = counted(); + for (int request = 0; request < 8; ++request) upload(); + const auto after = counted(); + Require(!HostImportCovers(window.importing, window.Address(0), 3 * piece), "a join of three 64 KiB ranges over a full budget with nothing evictable was imported"); + Require(after.refused - before.refused == 8 && after.remembered - before.remembered == 7, "8 uploads bound across three refused ranges scanned the imports for their join " + std::to_string(8 - (after.remembered - before.remembered)) + " times, not once"); + SetHostImportBudget(HostImportLiveBytes() + 3 * piece); + upload(); + Require(HostImportCovers(window.importing, window.Address(0), 3 * piece) && counted().made == after.made + 1, "the join was answered from the full budget's refusal after the budget grew"); +} + +void evictingRefusalStepDownTests(const Context& context) { + PieceWindow window(context); + constexpr auto piece = PieceWindow::Piece; + constexpr auto fillers = PieceWindow::Window / 2; + window.AddPieces(0, 4 * piece); + for (std::size_t filler = 0; filler < 4; ++filler) { + window.Add(fillers + 2 * filler * piece, piece); + Require(HostImportFor(window.importing, window.Address(fillers + 2 * filler * piece), 16) != nullptr, "a filler piece inside the import budget was not imported"); + } + SetHostImportBudget(HostImportLiveBytes() + 2 * piece); + { + GuestBufferMemory leased(window.importing); + leased.AcquireRegistered(); + leased.Upload(true); + leased.WriteBack(); + } + Require(window.Block(0, 2 * piece) && window.Serial(2 * piece) == 0, "the address-based build did not step down from the refused 256 KiB block to the 128 KiB block that fits"); + { + GuestBufferMemory upload(window.importing); + upload.Upload(false); + upload.WriteBack(); + } + const auto counted = [] { return HostImportCounters().requesters[static_cast(ImportRequester::Access)]; }; + const auto before = counted(); + for (int request = 0; request < 8; ++request) Require(HostImportFor(window.importing, window.Address(2 * piece), 16) == nullptr, "the rest of a block over a full budget with nothing evictable was imported"); + const auto after = counted(); + const auto scans = (after.refused - before.refused) - (after.remembered - before.remembered); + Require(after.refused - before.refused == 16 && scans == 1, "8 evicting requests that step down from a refused block scanned the imports " + std::to_string(scans) + " times, not once"); + SetHostImportBudget(HostImportLiveBytes() + piece); + Require(HostImportFor(window.importing, window.Address(2 * piece), 16) != nullptr && window.Block(2 * piece, piece), "a 64 KiB range was answered from a block's refusal remembered before the budget grew instead of stepping down to its own import"); +} + +void evictingRefusalWindowTests(const Context& context) { + PieceWindow window(context); + constexpr auto piece = PieceWindow::Piece; + constexpr auto fillers = PieceWindow::Window / 2; + SetHostImportWindow(0); + window.Add(0, piece); + window.Add(piece, piece); + for (std::size_t filler = 0; filler < 4; ++filler) { + window.Add(fillers + 2 * filler * piece, piece); + Require(HostImportFor(window.importing, window.Address(fillers + 2 * filler * piece), 16) != nullptr, "a filler piece inside the import budget was not imported"); + } + SetHostImportBudget(HostImportLiveBytes() + piece); + Require(HostImportFor(window.importing, window.Address(0), 16) == nullptr, "two adjacent 64 KiB ranges over the budget were imported without an import window"); + SetHostImportWindow(PieceWindow::Window); + Require(HostImportFor(window.importing, window.Address(0), 16) != nullptr && window.Block(0, piece), "a 64 KiB range was answered from a refusal made without the import window instead of stepping down once the window was set"); +} + +void importDeviceBudgetTests() { + constexpr std::uint64_t MiB = std::uint64_t{1} << 20u; + constexpr std::uint64_t GiB = std::uint64_t{1} << 30u; + VkPhysicalDeviceMemoryProperties memory{}; + memory.memoryHeapCount = 2; + memory.memoryHeaps[0] = {8 * GiB, VK_MEMORY_HEAP_DEVICE_LOCAL_BIT}; + memory.memoryHeaps[1] = {16 * GiB, 0}; + VkPhysicalDeviceMemoryBudgetPropertiesEXT reported{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT}; + const auto text = [](std::uint64_t bytes) { return std::to_string(bytes >> 20u) + " MiB"; }; + Require(HostImportBudgetFor(memory, nullptr, 6 * GiB, 0) == 6 * GiB, "an import budget without a memory budget report is not the configured one"); + Require(HostImportBudgetFor(memory, &reported, 6 * GiB, 0) == 6 * GiB, "a zero reported budget of the host heap did not leave the configured import budget"); + reported.heapBudget[1] = 12 * GiB; + reported.heapUsage[1] = 5 * GiB; + const auto granted = HostImportBudgetFor(memory, &reported, 10 * GiB, 2 * GiB); + Require(granted == 7 * GiB + 512 * MiB, "a 12 GiB host heap budget using 3 GiB besides 2 GiB of imports did not give 7.5 GiB to imports: " + text(granted)); + Require(HostImportBudgetFor(memory, &reported, 6 * GiB, 2 * GiB) == 6 * GiB, "a reported host heap budget raised the configured import budget"); + reported.heapUsage[1] = 5 * GiB + 10 * MiB; + const auto rounded = HostImportBudgetFor(memory, &reported, 10 * GiB, 2 * GiB); + Require(rounded == 7 * GiB + 448 * MiB, "the import budget from a reported heap was not rounded down to 64 MiB: " + text(rounded)); + reported.heapUsage[1] = 5 * GiB; + const auto uncounted = HostImportBudgetFor(memory, &reported, 64 * GiB, 8 * GiB); + Require(uncounted == 10 * GiB + 512 * MiB, "imports the heap usage does not count were taken from other usage: " + text(uncounted)); + reported.heapBudget[1] = 4 * GiB; + reported.heapUsage[1] = 6 * GiB; + Require(HostImportBudgetFor(memory, &reported, 6 * GiB, GiB) == 0, "a host heap used past its reported budget left room for imports"); + reported.heapBudget[0] = 1 * GiB; + reported.heapBudget[1] = 0; + Require(HostImportBudgetFor(memory, &reported, 6 * GiB, 0) == 6 * GiB, "the device-local heap's budget was taken for the host heap's"); + constexpr auto unreported = std::numeric_limits::max(); + Require(HostImportBudgetWith(6400 * MiB, true, 4480 * MiB, false, 0) == 6400 * MiB, "a host heap report lowered a set import budget"); + Require(HostImportBudgetWith(6400 * MiB, true, 4480 * MiB, true, 0) == 6400 * MiB, "a host heap report lowered a set import budget after a vkAllocateMemory failure"); + Require(HostImportBudgetWith(6400 * MiB, true, 4480 * MiB, true, 3752 * MiB) == 3752 * MiB, "the ceiling after a vkAllocateMemory failure did not lower a set import budget"); + Require(HostImportBudgetWith(5888 * MiB, false, 4480 * MiB, false, 0) == 5888 * MiB, "a host heap report lowered the default import budget before any vkAllocateMemory failure"); + Require(HostImportBudgetWith(5888 * MiB, false, 4480 * MiB, true, 0) == 4480 * MiB, "the default import budget did not take a lower host heap report after a vkAllocateMemory failure"); + Require(HostImportBudgetWith(5888 * MiB, false, 8 * GiB, true, 0) == 5888 * MiB, "a higher host heap report raised the default import budget"); + Require(HostImportBudgetWith(5888 * MiB, false, unreported, true, 0) == 5888 * MiB, "a device without a memory budget report changed the default import budget"); + Require(HostImportBudgetWith(5888 * MiB, false, 4480 * MiB, true, 3000 * MiB) == 3000 * MiB, "the ceiling below a host heap report did not hold the default import budget"); + Require(HostImportBudgetWith(5888 * MiB, false, 0, true, 0) != 0, "a host heap report that leaves imports no room made the default import budget 0, which the refusal memory reads as no budget"); +} + +void importCeilingTests(const Context& context, const BdaTestAccess& access, bool nearBudget) { + constexpr std::size_t piece = 65536; + constexpr std::size_t count = 24; + auto* block = static_cast(::operator new(2 * count * piece, std::align_val_t{piece})); + auto importing = context; + importing.hostImportAlignment = piece; + SetHostImportWindow(0); + ClearHostImports(importing.device); + { + GuestAllocations::Mutation mutation; + for (std::size_t index = 0; index < count; ++index) mutation.Add(block + 2 * index * piece, piece, true, true, true); + } + std::size_t next = 0; + const auto lookup = [&] { return HostImportFor(importing, reinterpret_cast(block + 2 * next++ * piece), 16) != nullptr; }; + const auto counted = [] { return HostImportCounters().requesters[static_cast(ImportRequester::Access)]; }; + const auto outcome = [](const HostImportRequesterCounts& before, const HostImportRequesterCounts& after) { return std::to_string(after.failed - before.failed) + " failed, " + std::to_string(after.refused - before.refused) + " refused"; }; + if (nearBudget) { + SetHostImportBudget(HostImportLiveBytes() + 2 * piece); + Require(lookup(), "the ceiling test's first import was not made"); + access.limitImports(0); + const auto before = counted(); + for (int attempt = 0; attempt < 3; ++attempt) static_cast(lookup()); + const auto after = counted(); + access.limitImports(std::nullopt); + Require(after.failed - before.failed == 1 && after.refused - before.refused == 2, "imports after a vkAllocateMemory failure within an eighth of the budget were not one failure and then refusals: " + outcome(before, after)); + } else { + SetHostImportBudget(64 * piece); + Require(lookup(), "the ceiling test's first import was not made"); + access.limitImports(0); + auto before = counted(); + Require(!lookup(), "an import past the mock's import limit was made"); + access.limitImports(std::nullopt); + Require(lookup() && counted().failed == before.failed + 1, "one vkAllocateMemory failure far below the import budget lowered it"); + access.limitImports(0); + before = counted(); + for (int attempt = 0; attempt < 5; ++attempt) static_cast(lookup()); + const auto after = counted(); + access.limitImports(std::nullopt); + Require(after.failed - before.failed == 3 && after.refused - before.refused == 2, "five imports after vkAllocateMemory failures far below the budget were not three failures and then refusals: " + outcome(before, after)); + } + Require(!lookup(), "an import was made over the lowered ceiling"); + SetHostImportBudget(0); + Require(lookup(), "a new import budget did not lift the ceiling"); + { + GuestAllocations::Mutation mutation; + for (std::size_t index = 0; index < count; ++index) mutation.Remove(block + 2 * index * piece); + } + ClearHostImports(importing.device); + ::operator delete(block, std::align_val_t{piece}); +} + +std::uint64_t reportedHostHeapBudget = 0; +std::uint64_t reportedHostHeapUsage = 0; + +void VKAPI_PTR reportHostHeap(VkPhysicalDevice, VkPhysicalDeviceMemoryProperties2* properties) { + for (auto* next = static_cast(properties->pNext); next != nullptr; next = next->pNext) { + if (next->sType != VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT) continue; + auto* reported = reinterpret_cast(next); + reported->heapBudget[1] = reportedHostHeapBudget; + reported->heapUsage[1] = reportedHostHeapUsage; + } +} + +void importReportFloorTests(const Context& context, const BdaTestAccess& access, bool setBudget) { + constexpr std::size_t piece = 65536; + constexpr std::size_t count = 24; + constexpr std::uint64_t GiB = std::uint64_t{1} << 30u; + static std::uintptr_t physical = 0x1000; + auto* block = static_cast(::operator new(2 * count * piece, std::align_val_t{piece})); + auto importing = context; + importing.hostImportAlignment = piece; + importing.memory.memoryHeapCount = 2; + importing.memory.memoryHeaps[0] = {8 * GiB, VK_MEMORY_HEAP_DEVICE_LOCAL_BIT}; + importing.memory.memoryHeaps[1] = {16 * GiB, 0}; + importing.memoryProperties2 = reportHostHeap; + importing.physical = reinterpret_cast(physical += 0x10); + reportedHostHeapBudget = 8 * (std::uint64_t{1} << 20u); + reportedHostHeapUsage = reportedHostHeapBudget; + SetHostImportWindow(0); + SetHostImportBudget(setBudget ? 64 * piece : 0); + ClearHostImports(importing.device); + { + GuestAllocations::Mutation mutation; + for (std::size_t index = 0; index < count; ++index) mutation.Add(block + 2 * index * piece, piece, true, true, true); + } + std::size_t next = 0; + const auto lookup = [&] { return HostImportFor(importing, reinterpret_cast(block + 2 * next++ * piece), 16) != nullptr; }; + const std::string budget = setBudget ? "a set import budget" : "the default import budget"; + for (int made = 0; made < 3; ++made) Require(lookup(), budget + " was lowered below " + std::to_string(made + 1) + " imports by a host heap report that leaves imports no room, before any vkAllocateMemory failure"); + access.limitImports(0); + Require(!lookup(), "an import past the mock's import limit was made"); + access.limitImports(std::nullopt); + if (setBudget) Require(lookup(), "a set import budget took the host heap report's limit after a vkAllocateMemory failure far below it"); + else Require(!lookup(), "the default import budget did not take the host heap report's limit after a vkAllocateMemory failure"); + SetHostImportBudget(0); + { + GuestAllocations::Mutation mutation; + for (std::size_t index = 0; index < count; ++index) mutation.Remove(block + 2 * index * piece); + } + ClearHostImports(importing.device); + ::operator delete(block, std::align_val_t{piece}); +} + +void importReportRefusalMemoryTests(const Context& context, const BdaTestAccess& access) { + constexpr std::size_t piece = 65536; + constexpr std::size_t count = 24; + constexpr std::uint64_t GiB = std::uint64_t{1} << 30u; + static std::uintptr_t physical = 0x800000; + auto* block = static_cast(::operator new(2 * count * piece, std::align_val_t{piece})); + auto* other = static_cast(::operator new(piece, std::align_val_t{piece})); + auto importing = context; + importing.hostImportAlignment = piece; + importing.memory.memoryHeapCount = 2; + importing.memory.memoryHeaps[0] = {8 * GiB, VK_MEMORY_HEAP_DEVICE_LOCAL_BIT}; + importing.memory.memoryHeaps[1] = {16 * GiB, 0}; + importing.memoryProperties2 = reportHostHeap; + importing.physical = reinterpret_cast(physical += 0x10); + reportedHostHeapBudget = 8 * (std::uint64_t{1} << 20u); + reportedHostHeapUsage = reportedHostHeapBudget; + SetHostImportWindow(0); + SetHostImportBudget(0); + ClearHostImports(importing.device); + { + GuestAllocations::Mutation mutation; + for (std::size_t index = 0; index < count; ++index) mutation.Add(block + 2 * index * piece, piece, true, true, true); + } + access.limitImports(0); + Require(HostImportFor(importing, reinterpret_cast(block), 16) == nullptr, "an import past the mock's import limit was made"); + access.limitImports(std::nullopt); + bool added = false; + const auto rebuild = [&] { + { + GuestAllocations::Mutation mutation; + if (added) mutation.Remove(other); + else mutation.Add(other, piece, true, true, false); + } + added = !added; + GuestBufferMemory leased(importing); + leased.AcquireRegistered(); + }; + const auto counted = [] { return HostImportCounters().requesters[static_cast(ImportRequester::Build)]; }; + const auto tries = [](const HostImportRequesterCounts& requested) { return requested.refused - requested.remembered; }; + auto before = counted(); + rebuild(); + auto after = counted(); + Require(after.made == before.made && tries(after) > tries(before), "an address-based build under a host heap report that leaves imports no room imported or refused nothing: " + std::to_string(after.made - before.made) + " made, " + std::to_string(tries(after) - tries(before)) + " refused"); + before = after; + rebuild(); + after = counted(); + Require(tries(after) == tries(before) && after.remembered > before.remembered, "a second address-based build under a host heap report that leaves imports no room tried " + std::to_string(tries(after) - tries(before)) + " refused ranges again instead of answering them from the refusal memory"); + { + GuestAllocations::Mutation mutation; + for (std::size_t index = 0; index < count; ++index) mutation.Remove(block + 2 * index * piece); + if (added) mutation.Remove(other); + } + ClearHostImports(importing.device); + ::operator delete(other, std::align_val_t{piece}); + ::operator delete(block, std::align_val_t{piece}); +} + void spaceReadAcrossImportsTests(const Context& context, const BdaTestAccess& access) { constexpr std::size_t piece = 65536; constexpr std::size_t window = 2 * piece; @@ -1799,6 +2646,57 @@ void sparseHeapTests(const Context& context, const BdaTestAccess& access) { releaseSparse(block, reserved); } +void spaceCommittedPagesTests(const Context& context, const BdaTestAccess& access) { + if (!AddressSpaceCounters().enabled) return; + constexpr std::size_t unit = 65536; + constexpr std::size_t reserved = 3 * unit; + auto* block = reserveSparse(reserved, unit); + std::memset(block, 0x31, unit); + const auto base = reinterpret_cast(block); + auto copying = context; + copying.hostImportAlignment = 0; + { + GuestAllocations::Mutation mutation; + mutation.Add(block, reserved, true, false, true); + } + const auto build = [&](std::byte expected, bool hit, const char* step) { + const auto before = AddressSpaceCounters(); + GuestBufferMemory leased(copying); + leased.AcquireRegistered(); + leased.AddReadable(base, 16); + leased.AddReadable(base + 2 * unit, 16); + leased.Upload(true); + std::uint32_t adjustment = 0; + const auto head = leased.Descriptor(base, 16, adjustment); + const auto headBytes = access.bytes(head.buffer); + Require(head.offset + adjustment < headBytes.size() && headBytes[head.offset + adjustment] == std::byte{0x31}, std::string("the committed head of a registered sparse range was not read: ") + step); + const auto tail = leased.Descriptor(base + 2 * unit, 16, adjustment); + const auto tailBytes = access.bytes(tail.buffer); + Require(tail.offset + adjustment < tailBytes.size() && tailBytes[tail.offset + adjustment] == expected, std::string("a cached address space served another commit state of its pages: ") + step); + leased.WriteBack(); + const auto after = AddressSpaceCounters(); + if (hit) Require(after.hits == before.hits + 1, std::string("the unchanged registry did not hit the cached address space: ") + step); + }; + build(std::byte{}, false, "first build"); + build(std::byte{}, true, "hit before the commit"); + { + GuestAllocations::Mutation mutation; +#ifdef _WIN32 + Require(VirtualAlloc(block + 2 * unit, unit, MEM_COMMIT, PAGE_READWRITE) != nullptr, "cannot commit the sparse range tail"); +#else + Require(mprotect(block + 2 * unit, unit, PROT_READ | PROT_WRITE) == 0, "cannot commit the sparse range tail"); +#endif + std::memset(block + 2 * unit, 0x72, unit); + } + build(std::byte{0x72}, false, "first build after the commit"); + build(std::byte{0x72}, true, "hit after the commit"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + releaseSparse(block, reserved); +} + bool RunSparseBufferHoleTests(const Context& context, const BdaTestAccess& access) { constexpr std::size_t unit = 65536; constexpr std::size_t copied = 3 * unit; @@ -2385,6 +3283,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; @@ -2488,10 +3407,38 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { importCoverageHintBudgetTests(context, access); failedImportJoinProbeBudgetTests(context, access); importWholeRangeOverWindowTests(context); + importMappedFailureGenerationTests(context); importStaleProbeTests(context, access); importRefusedWholeRangeTests(context); importNeighbouringEdgesTests(context); importAtMapOverBudgetTests(context); + importRequesterTests(context, access); + hostImportReportTests(); + importBlockMapTests(context, false); +#ifndef _WIN32 + importBlockMapTests(context, true); +#endif + importBlockBuildTests(context); + importBlockHoleTests(context); + importBlockUnalignedTests(context); + importBlockFlagTests(context); + importBlockRemapTests(context); + importBlockFailureTests(context, access); + importBlockBudgetTests(context); + importRefusalMemoryTests(context); + evictingRefusalMemoryTests(context); + evictingRefusalAgingTests(context); + evictingRefusalRangeTests(context); + evictingRefusalWindowTests(context); + evictingRefusalBlockRangesTests(context); + evictingRefusalJoinTests(context); + evictingRefusalStepDownTests(context); + importDeviceBudgetTests(); + importCeilingTests(context, access, false); + importCeilingTests(context, access, true); + importReportFloorTests(context, access, true); + importReportFloorTests(context, access, false); + importReportRefusalMemoryTests(context, access); heapMirrorTests(context, access); importedHeapMirrorTests(context, access); importWatchTests(); @@ -2499,6 +3446,7 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { gpuMappingTests(context); stridedOverhangTests(context, access); sparseHeapTests(context, access); + spaceCommittedPagesTests(context, access); hostWriteWatchExclusionTests(); sparseHeapMirrorTests(context, access); Require(AddressCopyOverflow({{0x1000, 0x3000, 0x2000, "uncommitted pages"}}, 0x2000).empty(), "copies within the limit were refused"); 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/BdaTests.hpp b/core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp index 73c6d9a28d..6aea082fff 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp @@ -12,6 +12,7 @@ struct BdaTestAccess { std::function(VkDeviceAddress)> addressBytes; std::function)> limitMemory; std::function allocationAttempts; + std::function)> limitImports; }; void RunBdaResourceTests(const AgcDriver::Graphics::Context& context, const BdaTestAccess& access); diff --git a/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp b/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp index 50356ef052..abdf8f0166 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp @@ -1,7 +1,9 @@ #include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "AllocationFailure.hpp" +#include "SlabReturn.hpp" #include +#include #include #include #include @@ -15,6 +17,7 @@ #include #include #include +#include #include using namespace AgcDriver::Graphics; @@ -177,6 +180,17 @@ VKAPI_ATTR void VKAPI_CALL mockMemoryProperties2(VkPhysicalDevice, VkPhysicalDev reported->heapUsage[0] = reportedUsage; } +std::uint64_t diagnosticBudgetQueries = 0; + +VKAPI_ATTR void VKAPI_CALL mockDiagnosticMemoryProperties2(VkPhysicalDevice, VkPhysicalDeviceMemoryProperties2* properties) { + ++diagnosticBudgetQueries; + auto* reported = static_cast(properties->pNext); + reported->heapUsage[0] = 1500 * MiB; + reported->heapBudget[0] = 2000 * MiB; + reported->heapUsage[1] = 2500 * MiB; + reported->heapBudget[1] = 3000 * MiB; +} + Context budgetContext() { auto context = mockContext(); context.physical = reinterpret_cast(std::uintptr_t{1}); @@ -776,6 +790,48 @@ void ImageCacheReclaim() { Expect(mock.liveBytes == 0, "a texture allocation leaked memory"); } +void AllocationFailureReportsHeapBudgets() { + mock = MockDevice{}; + diagnosticBudgetQueries = 0; + std::string failure; + std::uint64_t allocationPathBudgetQueries = 0; + { + auto context = mockContext(); + context.physical = reinterpret_cast(std::uintptr_t{1}); + context.memoryProperties2 = mockDiagnosticMemoryProperties2; + context.memory.memoryHeapCount = 2; + context.bufferPool = std::make_shared(context); + const auto beforeAllocation = diagnosticBudgetQueries; + mock.results = {VK_ERROR_DEVICE_LOST}; + try { + DeviceBuffer buffer(context, 4 * MiB, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + } catch (const std::exception& error) { + failure = error.what(); + } + allocationPathBudgetQueries = diagnosticBudgetQueries - beforeAllocation; + } + Expect(failure.find("heap 0 1500/2000 MiB") != std::string::npos, "allocation failure omitted the first heap's memory budget"); + Expect(failure.find("heap 1 2500/3000 MiB") != std::string::npos, "allocation failure omitted a later heap's memory budget"); + Expect(allocationPathBudgetQueries == 2, "allocation failure did not query the memory budget for both type selection and its diagnostic"); + + mock = MockDevice{}; + diagnosticBudgetQueries = 0; + failure.clear(); + { + auto context = mockContext(); + context.physical = reinterpret_cast(std::uintptr_t{1}); + context.memory.memoryHeapCount = 2; + mock.results = {VK_ERROR_DEVICE_LOST}; + try { + DeviceBuffer buffer(context, 4 * MiB, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + } catch (const std::exception& error) { + failure = error.what(); + } + } + Expect(failure.find("heap 0") == std::string::npos && failure.find("heap 1") == std::string::npos, "allocation failure emitted heap data when VK_EXT_memory_budget was unavailable"); + Expect(diagnosticBudgetQueries == 0, "missing-extension fallback attempted a memory budget query"); +} + void SuccessfulAllocationKeepsPool() { mock = MockDevice{}; { @@ -841,12 +897,126 @@ void DeviceBindingFailures() { } +VkDeviceMemory reclaimImage = VK_NULL_HANDLE; +const Context* reclaimImageContext = nullptr; + +VkDeviceSize mockImageReclaim(VkDeviceSize) { + if (reclaimImage == VK_NULL_HANDLE) return 0; + ReleaseImageMemory(*reclaimImageContext, reclaimImage, 10 * MiB, 0); + reclaimImage = VK_NULL_HANDLE; + return 10 * MiB; +} + +void ImageMemoryReclaim() { + mock = MockDevice{}; + auto context = mockContext(); + context.cacheReclaim = mockImageReclaim; + reclaimImageContext = &context; + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = 10 * MiB; + allocation.memoryTypeIndex = 0; + std::uint32_t type = 0; + reclaimImage = AllocateImageMemory(context, allocation, 1u, "test image", &type); + mock.limit = 12 * MiB; + auto request = allocation; + request.allocationSize = 4 * MiB; + const auto before = mock.attempts; + try { + const auto recovered = AllocateImageMemory(context, request, 1u, "test image", &type); + Expect(recovered != VK_NULL_HANDLE && mock.attempts == before + 2 && mock.liveBytes == 4 * MiB && context.bufferPool->RetainedImages() == 0, "the image memory a cache release returns to the pool is freed before the retry"); + ReleaseImageMemory(context, recovered, 4 * MiB, 0); + } catch (const std::exception& error) { + Expect(false, std::string("the cache release of an image did not permit recovery: ") + error.what()); + } + reclaimImageContext = nullptr; +} + +void ImageMemoryOverBudget() { + mock = MockDevice{}; + auto context = budgetContext(); + reportedBudget = 1000 * MiB; + reportedUsage = 500 * MiB; + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = 4 * MiB; + allocation.memoryTypeIndex = 0; + std::uint32_t type = 0; + const auto first = AllocateImageMemory(context, allocation, 1u, "test image", &type); + ReleaseImageMemory(context, first, 4 * MiB, 0); + Expect(mock.frees == 0 && context.bufferPool->RetainedImages() == 1, "image memory is retained while the heap is under its reported budget"); + reportedUsage = 1100 * MiB; + std::this_thread::sleep_for(std::chrono::milliseconds(150)); + const auto second = AllocateImageMemory(context, allocation, 1u, "test image", &type); + Expect(second == first && mock.allocations == 1, "a retained allocation is still handed out over the budget"); + ReleaseImageMemory(context, second, 4 * MiB, 0); + Expect(mock.frees == 1 && context.bufferPool->RetainedImages() == 0, "image memory released while the heap is over its reported budget is freed"); + const auto third = AllocateImageMemory(context, allocation, 1u, "test image", &type); + ReleaseImageMemory(context, third, 4 * MiB, 0); + reportedUsage = 500 * MiB; + std::this_thread::sleep_for(std::chrono::milliseconds(150)); + const auto fourth = AllocateImageMemory(context, allocation, 1u, "test image", &type); + ReleaseImageMemory(context, fourth, 4 * MiB, 0); + Expect(mock.frees == 2 && context.bufferPool->RetainedImages() == 1, "the pool retains image memory again once the heap is under its budget"); +} + +void ImageMemoryReuse() { + mock = MockDevice{}; + auto context = mockContext(); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = 4 * MiB; + allocation.memoryTypeIndex = 0; + std::uint32_t type = 99; + const auto first = AllocateImageMemory(context, allocation, 1u, "test image", &type); + Expect(type == 0 && mock.allocations == 1, "the first image allocates device memory of its type"); + ReleaseImageMemory(context, first, 4 * MiB, 0); + Expect(mock.frees == 0 && context.bufferPool->RetainedImages() == 1, "a released device-local image allocation is retained"); + const auto again = AllocateImageMemory(context, allocation, 1u, "test image", &type); + Expect(again == first && type == 0 && mock.allocations == 1 && context.bufferPool->RetainedImages() == 0, "the next image of the same type and size takes the retained allocation"); + auto smaller = allocation; + smaller.allocationSize = 2 * MiB; + const auto other = AllocateImageMemory(context, smaller, 1u, "test image", &type); + Expect(other != again && mock.allocations == 2, "an image of another size allocates"); + ReleaseImageMemory(context, other, 2 * MiB, 0); + ReleaseImageMemory(context, again, 4 * MiB, 0); + auto host = allocation; + host.memoryTypeIndex = 1; + const auto placed = AllocateImageMemory(context, host, 2u, "test image", &type); + Expect(type == 1 && mock.allocations == 3, "a host-visible image allocation is made"); + ReleaseImageMemory(context, placed, 4 * MiB, 1); + Expect(mock.frees == 1 && context.bufferPool->RetainedImages() == 2, "a host-visible image allocation is freed when released"); + const auto unsized = AllocateImageMemory(context, smaller, 1u, "test image", &type); + Expect(unsized == other && mock.allocations == 3 && context.bufferPool->RetainedImages() == 1, "an image of a retained size takes that allocation"); + ReleaseImageMemory(context, unsized, 0, 0); + Expect(mock.frees == 2 && context.bufferPool->RetainedImages() == 1, "an allocation of unknown size is freed"); + std::vector many; + for (int i = 0; i < 33; ++i) many.push_back(AllocateImageMemory(context, allocation, 1u, "test image", &type)); + Expect(many.front() == again && mock.allocations == 35, "a burst takes the retained allocation first and allocates the rest"); + for (const auto memory : many) ReleaseImageMemory(context, memory, 4 * MiB, 0); + Expect(context.bufferPool->RetainedImages() == 32 && mock.frees == 3 && !mock.memoryBytes.contains(many.front()) && mock.memoryBytes.contains(many.back()), "retained image memory is evicted least recently used under its budget"); + mock.results.push_back(VK_ERROR_OUT_OF_DEVICE_MEMORY); + auto third = allocation; + third.allocationSize = 3 * MiB; + const auto recovered = AllocateImageMemory(context, third, 1u, "test image", &type); + Expect(recovered != VK_NULL_HANDLE && mock.allocations == 36 && mock.frees == 35 && context.bufferPool->RetainedImages() == 0, "an allocation failure frees the retained image memory"); + ReleaseImageMemory(context, recovered, 3 * MiB, 0); + Expect(mock.frees == 35 && context.bufferPool->RetainedImages() == 1, "the pool retains image memory again after a trim"); +} + int main(int argc, char** argv) { try { if (argc == 2 && std::string_view(argv[1]) == "--small-device-retention") { SmallDeviceRetention(); return failures == 0 ? 0 : 1; } + if (argc == 2) { + const bool host = std::strcmp(argv[1], "--slab-return-host") == 0; + Expect(host || std::strcmp(argv[1], "--slab-return-device") == 0, "unknown slab return test mode"); + AgcDriver::Tests::SlabReturnWithoutAllocation(mockContext(), host ? 1u : 0u); + Expect(mock.liveBytes == 0, "slab return test leaked memory"); + return failures == 0 ? 0 : 1; + } + AgcDriver::Tests::SlabReturnWithoutAllocation(mockContext(), 0); + AgcDriver::Tests::SlabReturnWithoutAllocation(mockContext(), 1); + Expect(mock.liveBytes == 0, "slab return test leaked memory"); SizeClassesAndDirections(); LargerClass(); KeptUntilFence(); @@ -868,10 +1038,14 @@ int main(int argc, char** argv) { ReclaimSlabs(); AllocationFailure(VK_ERROR_DEVICE_LOST, VK_ERROR_DEVICE_LOST, true); AllocationFailure(VK_ERROR_OUT_OF_DEVICE_MEMORY, VK_ERROR_DEVICE_LOST, true); + AllocationFailureReportsHeapBudgets(); SuccessfulAllocationKeepsPool(); PartialSlabDoesNotRetry(); CacheReclaimRecovery(); ImageCacheReclaim(); + ImageMemoryReuse(); + ImageMemoryReclaim(); + ImageMemoryOverBudget(); HostPlacement(); RefusedDevicePlacement(); AttachmentReclaim(); 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..06d73e6578 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ConstantBitTest.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ConstantBitTest.cpp @@ -29,6 +29,122 @@ 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 factorFieldKept(bool multiplied) { + 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& masked = builder.Emit(IrOpcode::BitwiseAnd32, IrType::U32, {&sum, &builder.Constant(0xffffu)}); + auto& field = builder.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&masked, &builder.Constant(0u), &builder.Constant(24u)}); + auto& used = multiplied ? builder.IMul(field, field) : field; + auto& user = builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&used}); + static_cast(builder.Emit(IrOpcode::Return, IrType::Void, {})); + ConstantFolder().Fold(program); + const IrValue* result = user.Argument(0)->Resolve(); + if (!multiplied) return result == &masked; + return result->Opcode() == IrOpcode::IMul32 && result->Argument(0)->Resolve() == &field && field.Opcode() == IrOpcode::BitFieldUExtract && field.Argument(0)->Resolve() == &masked; +} + bool reversesTo(std::uint32_t word, std::uint32_t expected) { IrProgram program; auto& block = program.CreateBlock(); @@ -118,6 +234,14 @@ 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()); + passed &= run("a 24-bit field read of a value masked to 16 bits folds into the value", factorFieldKept(false)); + passed &= run("a 24-bit field read that a multiply uses is kept, even for a factor masked to 16 bits", factorFieldKept(true)); 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..3febba4f40 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp @@ -270,13 +270,13 @@ void verifyRequest(const char* path) { std::printf("%s: structured with %zu added blocks, %zu cloned instructions, %zu route variables\n", path, result.addedBlocks, result.clonedInstructions, result.routeVariables); } -std::size_t recompile(const std::string& name, std::span code, std::uint32_t subgroupSize) { +std::size_t recompile(const std::string& name, std::span code, std::uint32_t subgroupSize, std::uint32_t waveSize = 64u) { const std::array userData{0x10000000u, 0x00100000u, 0x40u, 0x00027facu}; const std::array capabilities{1u, 61u}; const std::array extensions{"SPV_KHR_storage_buffer_storage_class"}; RecompileRequest request{}; request.shader = {ShaderStage::Compute, 0x20000u, code, 0, {}}; - request.context.waveSize = 64; + request.context.waveSize = waveSize; request.context.userDataBaseRegister = 0; request.context.userData = userData; request.context.compute = ShaderComputeStageInfo{{64u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; @@ -296,6 +296,26 @@ std::size_t recompile(const std::string& name, std::span co return result.spirv.size(); } +void verifyCallLinkOverwrittenByWave64Compare() { + const std::vector code{0xbb080007u, 0xd4060008u, 0x0001e500u, 0xd5010001u, 0x0021e880u, 0xe0700000u, 0x80010100u, 0xbf810000u, 0xbe802008u, 0xbf9f0000u}; + static_cast(recompile("a wave64 compare that overwrites a call's link pair before a v_cndmask reads it", code, 64u)); +} + +void verifyCallLinkMaskReadByScalarInstruction() { + const std::array readers{0xbe8a2408u, 0xbe8a0408u, 0xbe8a0309u}; + for (const auto reader : readers) { + const std::vector code{0xbb080007u, 0xd4060008u, 0x0001e500u, reader, 0xbf800000u, 0xe0700000u, 0x80010100u, 0xbf810000u, 0xbe802008u, 0xbf9f0000u}; + static_cast(recompile("a wave64 scalar read of a compare mask in a call's link pair", code, 64u)); + bool refused = false; + try { + static_cast(recompile("a wave32 scalar read of a call's return address", code, 32u, 32u)); + } catch (const std::exception& error) { + refused = std::string_view(error.what()).find("escapes the constant-offset call/return model") != std::string_view::npos; + } + require(refused, "a wave32 program reading a call's return address high half was not refused"); + } +} + void verifyLargeLoopExitFallback() { const std::vector code{ 0x060202ffu, 0x3f800000u, 0x060202ffu, 0x3f000000u, 0x060202ffu, 0x3f001000u, 0x060202ffu, 0x3f002000u, @@ -471,6 +491,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,8 +1414,11 @@ done: } try { verifyNullSwappc(); + verifyScheduledJumpTable(); VerifyUserDataTail(); VerifyConstantHighTail(); + verifyCallLinkOverwrittenByWave64Compare(); + verifyCallLinkMaskReadByScalarInstruction(); verifyLargeLoopExitFallback(); verifyNestedExitForwarders(); verifyIrreducibleLoopEntry(); diff --git a/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp b/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp index e2932dc814..79878c2766 100644 --- a/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp @@ -341,7 +341,7 @@ void TestMultisampleDepthOwnership(Context context) { ClearDepthSurfaces(context.device); context.imageFormatProperties = sampledDepthProperties; Require(DepthSurfaceView(context, target) != VK_NULL_HANDLE, "a supported sampled MSAA format must retain attachment rendering without import eligibility"); - Require((lastImageUsage & VK_IMAGE_USAGE_SAMPLED_BIT) != 0, "a supported MSAA depth image must allow sampled views"); + Require((lastImageUsage & VK_IMAGE_USAGE_SAMPLED_BIT) == 0, "a native MSAA depth attachment must keep sampled usage off when the shadow path is available"); ClearDepthSurfaces(context.device); const auto destroyed = imagesDestroyed; auto owner = std::make_shared(context, newHandle(), newHandle()); diff --git a/core/libs/prx/libSceAgcDriver/tests/EmbeddedVertexFetch.cpp b/core/libs/prx/libSceAgcDriver/tests/EmbeddedVertexFetch.cpp index 46d1e6a891..5a1fddb472 100644 --- a/core/libs/prx/libSceAgcDriver/tests/EmbeddedVertexFetch.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/EmbeddedVertexFetch.cpp @@ -3,7 +3,9 @@ #include #include #include +#include #include +#include using namespace ShaderRecompiler; @@ -35,6 +37,54 @@ int Expect(const char* name, std::uint32_t source, std::uint32_t field, std::uin return 1; } +int NoCarryOffsets() { + const std::array code{ + 0x4a0a0a10u, 0x4a101011u, + 0xf4080506u, 0xfa000000u, + 0x02060b08u, + 0xbf8cc07fu, + 0xe0002000u, 0x80050a03u, + 0xbf8c3f70u, 0xbf810000u, + }; + const auto decoded = RdnaInstructionDecoder{}.Decode(code); + int failures = 0; + for (const auto wave : {32u, 64u}) { + const auto analyze = [&](const RdnaProgram& program) { return EmbeddedVertexFetchAnalyzer{}.Analyze(program, 6, 4, 8, 16, wave); }; + const auto plan = analyze(decoded); + if (plan.loads.size() != 1 || plan.vertexOffsetSgpr != 16 || plan.instanceOffsetSgpr != 17 || plan.vertexOffsetShared || plan.instanceOffsetShared || plan.vertexIndexObserved || plan.instanceIndexObserved) { + std::fprintf(stderr, "wave%u: no-carry offsets: loads=%zu vertex=%d instance=%d shared=%d/%d observed=%d/%d\n", wave, plan.loads.size(), plan.vertexOffsetSgpr, plan.instanceOffsetSgpr, plan.vertexOffsetShared, plan.instanceOffsetShared, plan.vertexIndexObserved, plan.instanceIndexObserved); + ++failures; + } + const auto rejected = [&](const char* label, std::initializer_list instruction) { + std::vector words(instruction); + words.insert(words.end(), code.begin() + 1, code.end()); + if (analyze(RdnaInstructionDecoder{}.Decode(words)).vertexOffsetSgpr == -1) return; + std::fprintf(stderr, "wave%u: %s was admitted as a no-carry offset\n", wave, label); + ++failures; + }; + rejected("VOP3 saturation", {0xd5258005u, 0x00020a10u}); + rejected("SDWA byte source", {0x4a0a0af9u, 0x06800610u}); + rejected("vector offset", {0x4a0a0b10u}); + rejected("different index source", {0x4a0a0c10u}); + rejected("non-user scalar", {0x4a0a0a1fu}); + rejected("subtraction", {0x4c0a0a10u}); + auto sharedCode = std::vector(code.begin(), code.end()); + sharedCode.insert(sharedCode.end() - 1, 0x7e080210u); + const auto shared = analyze(RdnaInstructionDecoder{}.Decode(sharedCode)); + if (shared.vertexOffsetSgpr != 16 || !shared.vertexOffsetShared) { + std::fprintf(stderr, "wave%u: a second reader of the offset was not marked shared\n", wave); + ++failures; + } + auto observedCode = std::vector(code.begin(), code.end()); + observedCode.insert(observedCode.begin(), 0x7e020305u); + if (!analyze(RdnaInstructionDecoder{}.Decode(observedCode)).vertexIndexObserved) { + std::fprintf(stderr, "wave%u: a read before the offset add was not preserved\n", wave); + ++failures; + } + } + return failures; +} + } int main() { @@ -43,5 +93,6 @@ int main() { failures += Expect("width 8 at offset 4", 0xfff00320u, 0x00080004u, 0x32u); failures += Expect("width 0", 0x00000320u, 0x00000000u, 0u); failures += Expect("width above 32", 0x00000320u, 0x007f0000u, 0x320u); + failures += NoCarryOffsets(); return failures == 0 ? 0 : 1; } diff --git a/core/libs/prx/libSceAgcDriver/tests/F32DenormPreserve.cpp b/core/libs/prx/libSceAgcDriver/tests/F32DenormPreserve.cpp new file mode 100644 index 0000000000..0f3fe85da7 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/F32DenormPreserve.cpp @@ -0,0 +1,95 @@ +#include "Recompiler.hpp" +#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); +} + +constexpr std::array CvtF16F32Store{0x7e021500u, 0xe0700000u, 0x80000100u, 0xbf810000u}; + +std::vector recompile(bool preserve, bool flush, bool flushCapability = true) { + const std::array capabilities{spv::CapabilityShader, spv::CapabilityGroupNonUniform, spv::CapabilityDenormPreserve, spv::CapabilityDenormFlushToZero}; + const std::array userData{0x10000000u, 0x00100000u, 0x40u, 0x00027facu}; + const std::array extensions{"SPV_KHR_storage_buffer_storage_class", "SPV_KHR_float_controls"}; + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, 0x20000u, CvtF16F32Store, 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 = std::span(capabilities).first(flushCapability ? capabilities.size() : capabilities.size() - 1); + request.target.supportedExtensions = extensions; + request.target.maxWorkgroupSize = {1024u, 1024u, 64u}; + request.target.maxWorkgroupInvocations = 1024; + request.target.maxWorkgroupSharedMemoryBytes = 49152; + request.target.float32DenormPreserve = preserve; + request.target.float32FlushToZero = flush; + 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(); +} + +struct Modes { + bool preserve32 = false; + bool flush32 = false; + bool preserveCapability = false; +}; + +Modes scan(std::span words) { + Modes modes; + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> 16u; + const auto op = static_cast(words[cursor] & 0xffffu); + require(count != 0 && cursor + count <= words.size(), "truncated SPIR-V instruction"); + if (op == spv::OpCapability && words[cursor + 1] == spv::CapabilityDenormPreserve) modes.preserveCapability = true; + if (op == spv::OpExecutionMode && count == 4 && words[cursor + 3] == 32u) { + if (words[cursor + 2] == spv::ExecutionModeDenormPreserve) modes.preserve32 = true; + if (words[cursor + 2] == spv::ExecutionModeDenormFlushToZero) modes.flush32 = true; + } + cursor += count; + } + return modes; +} + +void verify() { + const auto preserved = scan(recompile(true, false)); + require(preserved.preserve32 && preserved.preserveCapability && !preserved.flush32, "float32DenormPreserve did not emit the 32-bit DenormPreserve execution mode and capability"); + const auto plain = scan(recompile(false, false)); + require(!plain.preserve32 && !plain.flush32, "a target without float32DenormPreserve or float32FlushToZero set a 32-bit denormal execution mode"); + const auto flushed = scan(recompile(true, true)); + require(flushed.flush32 && !flushed.preserve32, "the hardware 32-bit flush did not take precedence over the preserve flag"); + const auto nativeFlush = scan(recompile(true, true, false)); + require(!nativeFlush.preserve32 && !nativeFlush.flush32, "a hardware flush without the DenormFlushToZero capability declared a 32-bit denormal execution mode"); +} + +} + +int main() { + try { + verify(); + } catch (const std::exception& error) { + std::fprintf(stderr, "F32DenormPreserve: %s\n", error.what()); + return 1; + } + std::puts("F32DenormPreserve: the 32-bit DenormPreserve execution mode follows the target flag and yields to the hardware flush"); + return 0; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/Flip.cpp b/core/libs/prx/libSceAgcDriver/tests/Flip.cpp index fd1e548a00..ec73dea01a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Flip.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Flip.cpp @@ -136,7 +136,6 @@ void testFlipAndBoundary() { expectFailure([&] { AgcDriverRegisterVideoOutput_nid_postfix(7, output); }); std::array words{0xc004105c, 7, 0xfffffffeu, 1, 0, 0}; Packet packet{words.data(), 6, 0, {}}; - expectFailure([&] { sceAgcDriverSubmitAcb(0x20, &packet); }); words[0] = 0xc004105d; expectFailure([&] { sceAgcDriverSubmitDcb(&packet); }); words[0] = 0xc004105c; @@ -322,6 +321,18 @@ void testComputeRenderingWait() { AgcDriverUnregisterVideoOutput_nid_postfix(7, output); } +void testComputeFlip() { + auto output = std::make_shared(); + AgcDriverRegisterVideoOutput_nid_postfix(7, output); + std::array words{0xc004105c, 7, 0xfffffffeu, 1, 0x76543211u, 0xfedcba98u}; + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + check(sceAgcDriverSubmitAcb(0x20, &packet) == 0, "compute flip submission failed"); + AgcDriverWaitIdle_nid_postfix(); + check(output->state->ready == 1 && output->state->failed == 0, "a flip submitted on a compute queue was not handed to its output"); + check(output->state->last.argument == -0x123456789abcdefLL && output->state->last.index == -2 && output->state->last.mode == 1, "decoded compute flip arguments changed"); + AgcDriverUnregisterVideoOutput_nid_postfix(7, output); +} + void testFailure() { auto output = std::make_shared(); output->state->fail = true; @@ -423,7 +434,7 @@ int main(int argc, char** argv) { return 0; } if (argc == 2) testReset(std::string(argv[1]) == "compute"); - else { testFlipAndBoundary(); testFlipHold(); testComputeRenderingWait(); testFailure(); } + else { testFlipAndBoundary(); testFlipHold(); testComputeRenderingWait(); testComputeFlip(); testFailure(); } const auto shutdown = expectFailure([] { LibcRunShutdown_nid_postfix(); }); check(shutdown.find(argc == 2 ? (std::string(argv[1]) == "compute" ? "not readable" : "required shader register") : "intentional flip failure") != std::string::npos, "shutdown lost worker failure"); std::puts("AGC flip and suspend tests passed"); diff --git a/core/libs/prx/libSceAgcDriver/tests/Float32FlushControl.cpp b/core/libs/prx/libSceAgcDriver/tests/Float32FlushControl.cpp new file mode 100644 index 0000000000..f51d28a2bf --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/Float32FlushControl.cpp @@ -0,0 +1,37 @@ +#include "prx/libSceAgcDriver/Execution/include/Float32FlushControl.hpp" +#include + +namespace { + +struct Case { + VkDriverId driver; + VkBool32 flushToZero; + VkShaderFloatControlsIndependence independence; + bool probe; + bool declared; +}; + +constexpr Case Cases[]{ + {VK_DRIVER_ID_MOLTENVK, VK_FALSE, VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_NONE, true, false}, + {VK_DRIVER_ID_MOLTENVK, VK_TRUE, VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_ALL, true, true}, + {VK_DRIVER_ID_MESA_RADV, VK_TRUE, VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_ALL, true, true}, + {VK_DRIVER_ID_MESA_RADV, VK_TRUE, VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_32_BIT_ONLY, true, true}, + {VK_DRIVER_ID_MESA_RADV, VK_TRUE, VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_NONE, false, false}, + {VK_DRIVER_ID_NVIDIA_PROPRIETARY, VK_FALSE, VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_ALL, false, false}, + {VK_DRIVER_ID_AMD_PROPRIETARY, VK_TRUE, VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_32_BIT_ONLY, true, true}, + {static_cast(0), VK_FALSE, VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_NONE, false, false}, +}; + +} + +int main() { + int failures = 0; + for (const auto& item : Cases) { + const auto control = AgcDriver::Float32FlushControlFor(item.driver, item.flushToZero, item.independence); + if (control.probe != item.probe || control.declared != item.declared) { + std::fprintf(stderr, "driver %d flush %u independence %d: expected probe %d declared %d, got %d %d\n", static_cast(item.driver), static_cast(item.flushToZero), static_cast(item.independence), item.probe ? 1 : 0, item.declared ? 1 : 0, control.probe ? 1 : 0, control.declared ? 1 : 0); + ++failures; + } + } + return failures == 0 ? 0 : 1; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 69e85bb7d7..b435118f3d 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 @@ -519,6 +521,12 @@ void stateTests() { queue.shader[0x008] = 0; Require(AgcDriver::Graphics::PixelProgramSkipped(queue), "a zero pixel program address was not read as unset"); Require(AgcDriver::Graphics::DrawRejection(queue, true).find("writes color") != std::string::npos, "a draw without a pixel program that writes color was accepted"); + auto colorDisabled = queue; + colorDisabled.context[0x202] = colorDisabled.context.at(0x202) & ~0x70u; + Require(AgcDriver::Graphics::DrawRejection(colorDisabled, true).empty(), "a draw without a pixel program was rejected while CB_COLOR_CONTROL disables color"); + Require(!AgcDriver::Graphics::DecodeState(colorDisabled).hasColorTarget, "a draw with CB_COLOR_CONTROL disabling color decoded a color target"); + colorDisabled.context.erase(0x202); + Require(AgcDriver::Graphics::DrawRejection(colorDisabled, true).find("writes color") != std::string::npos, "a draw without a pixel program and an unwritten CB_COLOR_CONTROL was accepted"); queue.shader.erase(0x008); queue.shader.erase(0x009); Require(AgcDriver::Graphics::PixelProgramSkipped(queue), "an unwritten pixel program address was not read as unset"); @@ -1076,7 +1084,7 @@ void ShaderStageTests() { lines.context[0x29b] = 2; Require(AgcDriver::Graphics::DecodeState(lines).stages.mesh->primitivesPerGroup == 17, "triangle input with a triangle-strip output and a triangle polygon offset did not decode"); lines.context[0x29b] = 1; - expectFailure([&] { AgcDriver::Graphics::DecodeState(lines); }, "polygon mode, depth bias or nonstandard rasterization"); + expectFailure([&] { AgcDriver::Graphics::DecodeState(lines); }, "depth bias or nonstandard rasterization"); } queue.context[0x2ab] = 0; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "invalid VGT_ESGS_RING_ITEMSIZE"); @@ -1673,7 +1681,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); @@ -2341,7 +2354,8 @@ void DepthStencilTests() { const auto firstSlice = AgcDriver::Graphics::DecodeState(queue); Require(firstSlice.color.address == reinterpret_cast(colorMemory.data()), "a color view of several slices did not retain its first slice"); queue.context[0x207] = 1u << 18u; - expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "layered draws into a color view of several array slices"); + const auto layeredSingle = AgcDriver::Graphics::DecodeState(queue); + Require(layeredSingle.color.address == firstSlice.color.address && layeredSingle.color.arrayAddress == firstSlice.color.address && layeredSingle.color.arrayLastSlice == 1u && layeredSingle.color.layers == 2u && layeredSingle.renderLayers == 2u, "a layered draw into slices 0 to 1 of a surface of one slice did not render them as a two-slice array"); queue.context[0x207] = 0; const auto sliced = reinterpret_cast(sliceMemory.data()); queue.context[0x318] = static_cast(sliced >> 8u); @@ -2356,7 +2370,8 @@ void DepthStencilTests() { const auto range = AgcDriver::Graphics::DecodeState(queue); Require(range.color.address == sliced && range.color.bytes == 1024u, "a color view of slices 0 to 1 of a 2D array target did not render into its first slice"); queue.context[0x207] = 1u << 18u; - expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "layered draws into a color view of several array slices"); + const auto layeredArray = AgcDriver::Graphics::DecodeState(queue); + Require(layeredArray.color.address == sliced && layeredArray.color.arraySlice == 0u && layeredArray.color.layers == 2u && layeredArray.renderLayers == 2u && layeredArray.layerExports != 0u, "a layeredArray draw into slices 0 to 1 of a 2D array target did not render two layers"); queue.context[0x207] = 0; queue.context[0x31b] = 2u | (2u << 13u); expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "beyond the array surface"); @@ -2412,6 +2427,26 @@ void OneDimensionalColorTests() { Require(line.color.extent.width == 64u && line.color.extent.height == 1u && line.color.address == reinterpret_cast(colorMemory.data()) && line.color.depth == 1u, "a 1D color target did not decode as one row"); } +void multisampledSingleClearTests() { + static constexpr std::array kernel{0xbe800303u, 0xbe830382u, 0xf40c0401u, 0xfa000000u, 0xd7460000u, 0x04010609u, 0xd7460001u, 0x0405060au, 0x7e060208u, 0x7e0c020bu, 0x7e0e0280u, 0xd5690004u, 0x02000100u, 0x7e000205u, 0xd5690005u, 0x02000901u, 0xd7000001u, 0x02000c80u, 0xbf8cc07fu, 0xf0201f38u, 0x00040004u, 0xbf810000u}; + const std::array userData{0u, 0u, 0x0f7f0008u, 8u, 8u, 0xff00ffccu, 0u, 0u, 0u}; + const std::array group{8, 8, 1}; + const auto match = [&](std::span code, std::span words, const std::array& threads) { + return AgcDriver::Graphics::MatchMultisampledSingleClear(code, words, threads); + }; + const auto clear = match(kernel, userData, group); + Require(clear.has_value() && clear->descriptor == 0x20f7f0008ull && clear->color == std::array{0xff00ffccu, 0u, 0u, 0u}, "RADV's multisampled comp-to-single clear was not recognized"); + Require(!match(kernel, userData, {64, 1, 1}), "the comp-to-single clear matched with another group size"); + Require(!match(kernel, std::span(userData).first(8), group), "the comp-to-single clear matched without its clear color"); + auto unsized = userData; + unsized[3] = 0; + Require(!match(kernel, unsized, group), "a comp-to-single clear with no block size was recognized"); + auto other = kernel; + other[17] ^= 1u; + Require(!match(other, userData, group), "a different kernel was taken for RADV's comp-to-single clear"); + Require(!match(std::span(kernel).first(21), userData, group), "a truncated kernel was taken for RADV's comp-to-single clear"); +} + void htileDepthClearTests() { static constexpr std::array kernel{0xd7460000u, 0x04010c06u, 0x34000084u, 0xdc388000u, 0x04020000u, 0xbf8c3f70u, 0xd7710008u, 0x00120805u, 0xd7710009u, 0x00120a05u, 0xd771000au, 0x00120c05u, 0xd771000bu, 0x00120e05u, 0xdc788000u, 0x00020800u, 0xbf810000u}; const std::array wave{64, 1, 1}; @@ -2594,6 +2629,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); @@ -2632,9 +2689,38 @@ void DepthBoundsBiasTests() { expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "window offset"); queue.context[0x80] = 0; queue.userConfig[0x242] = 2; - expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "polygon mode, depth bias or nonstandard rasterization"); + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "depth bias or nonstandard rasterization"); queue.userConfig[0x242] = 3; - expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "polygon mode, depth bias or nonstandard rasterization"); + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "depth bias or nonstandard rasterization"); + queue.userConfig[0x242] = 4; + queue.context[0x280] = 0x80008u; + queue.context[0x281] = 0xffff0000u; + queue.context[0x282] = 8u; + const auto polygonMode = [&](std::uint32_t raster) { + queue.context[0x205] = raster; + return AgcDriver::Graphics::DecodeState(queue).polygonMode; + }; + Require(polygonMode(0x8u) == VK_POLYGON_MODE_POINT, "POLY_MODE with point fronts and backs did not decode to the point polygon mode"); + Require(polygonMode(0x128u) == VK_POLYGON_MODE_LINE, "POLY_MODE with line fronts and backs did not decode to the line polygon mode"); + Require(polygonMode(0x248u) == VK_POLYGON_MODE_FILL, "POLY_MODE with filled fronts and backs did not decode to the fill polygon mode"); + Require(polygonMode(0x20u) == VK_POLYGON_MODE_FILL, "polygon types without POLY_MODE did not keep the triangles filled"); + Require(polygonMode(0x4au) == VK_POLYGON_MODE_FILL, "the point type of culled back faces changed the filled front faces"); + Require(polygonMode(0x109u) == VK_POLYGON_MODE_LINE, "culled front faces did not leave the back faces' line type"); + expectFailure([&] { polygonMode(0x48u); }, "different front and back polygon modes"); + expectFailure([&] { polygonMode(0x18u); }, "a reserved POLY_MODE"); + expectFailure([&] { polygonMode(0x68u); }, "a reserved polygon type"); + expectFailure([&] { polygonMode(0x808u); }, "depth bias with a point or line polygon mode"); + queue.context[0x280] = 0x100010u; + expectFailure([&] { polygonMode(0x8u); }, "a point polygon mode with a point size other than one pixel"); + queue.context[0x280] = 0x80008u; + queue.context[0x281] = 0xffff0010u; + expectFailure([&] { polygonMode(0x8u); }, "a point polygon mode whose size limits exclude one pixel"); + queue.context[0x281] = 0xffff0000u; + queue.context[0x282] = 0x10u; + expectFailure([&] { polygonMode(0x128u); }, "a line polygon mode wider or narrower than one pixel"); + queue.context[0x282] = 8u; + queue.userConfig[0x242] = 2; + Require(polygonMode(0x8u) == VK_POLYGON_MODE_FILL, "a line list took the point polygon mode of its triangles"); queue.userConfig[0x242] = 4; queue.context[0x205] = 0x00001a4au; queue.context[0x2de] = 0x1f0u; @@ -3690,6 +3776,83 @@ void multisampleTests() { Require(MultisampledFillClearColor(srgb, 0x80ffffffu, clear) && clear.float32[2] == 1.0f && clear.float32[3] == 128.0f / 255.0f, "an sRGB fill of white with linear alpha 0x80 did not clear to it"); Require(!MultisampledFillClearColor(srgb, 0x00000080u, clear), "an sRGB fill whose color decodes inexactly cleared"); + using AgcDriver::Graphics::MultisampledDccClear; + auto single = srgb; + single.format = VK_FORMAT_R8G8B8A8_UNORM; + single.tileMode = ColorTileMode::RenderTarget; + single.extent = {581, 729}; + single.address = single.surfaceAddress = 0x40010000; + single.dccAddress = 0x50010000; + single.dccPipeAligned = true; + const std::vector singleKeys(AgcDriver::Graphics::MultisampledDccBytes(single), std::byte{0x10}); + expectFailure([&] { MultisampledDccClear(single, singleKeys, {}); }, "comp-to-single keys without a recorded clear color"); + AgcDriver::Graphics::GuestTextureResource cleared{}; + cleared.baseAddress = single.surfaceAddress; + cleared.width = 581; + cleared.height = 729; + cleared.samples = 4; + AgcDriver::Graphics::NoteMultisampledSingleClear(cleared, {0xff00ffccu, 0u, 0u, 0u}); + clear = MultisampledDccClear(single, singleKeys, {}); + Require(clear.float32[0] == 204.0f / 255.0f && clear.float32[1] == 1.0f && clear.float32[2] == 0.0f && clear.float32[3] == 1.0f, "comp-to-single keys did not clear to the recorded RGBA8 color"); + auto resized = single; + resized.extent = {580, 729}; + const std::vector resizedKeys(AgcDriver::Graphics::MultisampledDccBytes(resized), std::byte{0x10}); + expectFailure([&] { MultisampledDccClear(resized, resizedKeys, {}); }, "comp-to-single keys without a recorded clear color"); + auto wide = halfFloat; + wide.surfaceAddress = 0x40000000; + wide.dccAddress = 0x50000000; + wide.dccPipeAligned = true; + AgcDriver::Graphics::GuestTextureResource wideCleared{}; + wideCleared.baseAddress = wide.surfaceAddress; + wideCleared.width = wide.extent.width; + wideCleared.height = wide.extent.height; + wideCleared.samples = wide.samples; + AgcDriver::Graphics::NoteMultisampledSingleClear(wideCleared, {0x3c003c00u, 0x3c003c00u, 0u, 0u}); + const std::vector wideKeys(AgcDriver::Graphics::MultisampledDccBytes(wide), std::byte{0x10}); + clear = MultisampledDccClear(wide, wideKeys, {}); + Require(clear.float32[0] == 1.0f && clear.float32[1] == 1.0f && clear.float32[2] == 1.0f && clear.float32[3] == 1.0f, "comp-to-single keys did not clear to the recorded RGBA16F color"); + auto widest = wide; + widest.format = VK_FORMAT_R32G32B32A32_SFLOAT; + widest.elementBytes = 16; + widest.address = widest.surfaceAddress = 0x48000000; + widest.dccAddress = 0x58000000; + auto widestCleared = wideCleared; + widestCleared.baseAddress = widest.surfaceAddress; + AgcDriver::Graphics::NoteMultisampledSingleClear(widestCleared, {0x3f800000u, 0x3f000000u, 0u, 0x3f800000u}); + const std::vector widestKeys(AgcDriver::Graphics::MultisampledDccBytes(widest), std::byte{0x10}); + clear = MultisampledDccClear(widest, widestKeys, {}); + Require(clear.float32[0] == 1.0f && clear.float32[1] == 0.5f && clear.float32[2] == 0.0f && clear.float32[3] == 1.0f, "comp-to-single keys did not clear to the recorded RGBA32F color"); + auto swizzled = single; + swizzled.pipeBankXor = 0x4000; + expectFailure([&] { MultisampledDccClear(swizzled, singleKeys, {}); }, "comp-to-single keys without a recorded clear color"); + constexpr std::size_t reusedBytes = 0x10000; + auto* reusedBlock = static_cast(::operator new(reusedBytes, std::align_val_t{reusedBytes})); + const auto mapReused = [&] { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, reusedBlock, reusedBytes, true, true, true); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + const auto unmapReused = [&] { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, reusedBlock); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + mapReused(); + auto reused = single; + reused.address = reused.surfaceAddress = reinterpret_cast(reusedBlock); + auto reusedCleared = cleared; + reusedCleared.baseAddress = reused.surfaceAddress; + AgcDriver::Graphics::NoteMultisampledSingleClear(reusedCleared, {0xff00ffccu, 0u, 0u, 0u}); + clear = MultisampledDccClear(reused, singleKeys, {}); + Require(clear.float32[0] == 204.0f / 255.0f && clear.float32[3] == 1.0f, "comp-to-single keys on mapped guest memory did not clear to the recorded color"); + unmapReused(); + mapReused(); + expectFailure([&] { MultisampledDccClear(reused, singleKeys, {}); }, "comp-to-single keys without a recorded clear color"); + clear = MultisampledDccClear(single, singleKeys, {}); + Require(clear.float32[0] == 204.0f / 255.0f, "unmapping one surface dropped another surface's comp-to-single clear"); + unmapReused(); + ::operator delete(reusedBlock, std::align_val_t{reusedBytes}); + using AgcDriver::Graphics::TakeMultisampledClear; halfFloat.clearWords = {0x3c003c00u, 0x3c003c00u}; MultisampledCmask clearedCmask(0x50000000, 0x1000); @@ -4159,7 +4322,7 @@ void DepthClipTests() { collapsedQueue.context[0x10f] = std::bit_cast(-1.0f); expectFailure([&] { AgcDriver::Graphics::DecodeState(collapsedQueue); }, "unsupported viewport transform"); for (std::uint32_t bit = 0; bit < 32; ++bit) { - if (bit == 19 || bit == 24 || bit == 26 || bit == 27) continue; + if (bit == 16 || bit == 19 || bit == 24 || bit == 26 || bit == 27) continue; for (const auto linearBit : {0u, 0x01000000u}) { queue.context[0x204] = (1u << bit) | linearBit; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "PA_CL_CLIP_CNTL"); @@ -4172,10 +4335,12 @@ void DepthClipTests() { for (const auto clip : {0x00080000u, 0x00000000u, 0x0c080000u, 0x04000000u}) { queue.context[0x204] = clip; const auto plain = AgcDriver::Graphics::DecodeState(queue); - queue.context[0x204] = clip | 0x01000000u; - const auto linearClip = AgcDriver::Graphics::DecodeState(queue); - Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck rejected DX_LINEAR_ATTR_CLIP_ENA"); - Require(linearClip.negativeOneToOne == plain.negativeOneToOne && linearClip.depthClamp == plain.depthClamp && linearClip.viewport.minDepth == plain.viewport.minDepth && linearClip.viewport.maxDepth == plain.viewport.maxDepth, "DX_LINEAR_ATTR_CLIP_ENA changed the clip space, depth clamping or depth range"); + for (const auto extraBit : {0x00010000u, 0x01000000u, 0x01010000u}) { + queue.context[0x204] = clip | extraBit; + const auto decoded = AgcDriver::Graphics::DecodeState(queue); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck rejected CLIP_DISABLE or DX_LINEAR_ATTR_CLIP_ENA"); + Require(decoded.negativeOneToOne == plain.negativeOneToOne && decoded.depthClamp == plain.depthClamp && decoded.viewport.minDepth == plain.viewport.minDepth && decoded.viewport.maxDepth == plain.viewport.maxDepth, "CLIP_DISABLE or DX_LINEAR_ATTR_CLIP_ENA changed the clip space, depth clamping or depth range"); + } } } @@ -4369,6 +4534,7 @@ struct MockVulkan { VkDeviceSize allocatedBytes = 0; std::optional memoryLimit; std::uint64_t allocationAttempts = 0; + std::optional importLimit; std::uint32_t bufferMemoryTypeBits = 1; std::vector allocationTypes; std::vector layoutBindings; @@ -4440,6 +4606,7 @@ VKAPI_ATTR VkResult VKAPI_CALL mockAllocateMemory(VkDevice, const VkMemoryAlloca ++mock.allocationAttempts; mock.allocationTypes.push_back(info->memoryTypeIndex); if (mock.memoryLimit.has_value() && mock.allocatedBytes + info->allocationSize > *mock.memoryLimit) return VK_ERROR_OUT_OF_DEVICE_MEMORY; + if (mock.importLimit.has_value() && info->allocationSize > *mock.importLimit && info->pNext != nullptr && static_cast(info->pNext)->pNext != nullptr) return VK_ERROR_OUT_OF_DEVICE_MEMORY; *memory = makeHandle(); mock.memories[*memory] = std::vector(info->allocationSize); mock.allocationSizes[*memory] = info->allocationSize; @@ -4810,6 +4977,7 @@ AgcDriver::Graphics::Context mockContext() { context.limits.maxPerStageDescriptorStorageBuffers = 16; context.limits.maxPerStageResources = 128; context.limits.maxDescriptorSetStorageBuffers = 32; + context.limits.maxImageArrayLayers = 2048; return context; } @@ -5743,6 +5911,16 @@ void resourceTests() { expectSingleAccepted(changed([](auto& binding) { binding.guestDescriptor[2] = 32768; }), "an unregistered V# past the range limit"); expectSingleAccepted(changed([](auto& binding) { binding.guestDescriptor = vsharp(reinterpret_cast(0x1000), 8); }), "an unmapped V#"); expectSingleFailure(changed([&](auto& binding) { binding.guestDescriptor = vsharp(reinterpret_cast(state.color.address), 64); }), "aliases the render target"); + const auto stridedBefore = [&](std::uint32_t past) { + constexpr std::uint32_t stride = 60; + constexpr std::uint32_t records = 4; + auto descriptor = vsharp(reinterpret_cast(state.color.address - (stride * records - past)), records); + descriptor[1] |= stride << 16u; + return descriptor; + }; + expectSingleAccepted(changed([&](auto& binding) { binding.guestDescriptor = stridedBefore(8); binding.bufferWritten = {false}; }), "a read-only strided V# whose last record runs 8 bytes into the render target"); + expectSingleFailure(changed([&](auto& binding) { binding.guestDescriptor = stridedBefore(8); binding.bufferWritten = {true}; }), "aliases the render target"); + expectSingleFailure(changed([&](auto& binding) { binding.guestDescriptor = stridedBefore(60); }), "aliases the render target"); expectSingleAccepted(changed([](auto& binding) { binding.count = 3; binding.guestDescriptor = join(join(vsharp(guestFirst.data(), 16), vsharp(guestSecond.data(), 32)), vsharp(reinterpret_cast(0x1000), 8)); }), "an unmapped V# element"); expectSingleFailure(changed([&](auto& binding) { binding.count = 2; binding.guestDescriptor = join(vsharp(guestFirst.data(), 16), vsharp(reinterpret_cast(state.color.address), 64)); }), "aliases the render target"); expectSingleFailure(changed([](auto& binding) { binding.count = 17; binding.guestDescriptor.assign(68, 0); }), "per-stage limits"); @@ -6397,10 +6575,10 @@ void storageTextureBudgetTests() { Require(StorageTextureBudget(memory, &reported, 0, 0) == 2560 * MiB, "a 10 GiB budget on the 16 GiB heap does not limit the storage image cache to a quarter of the budget"); reported.heapUsage[2] = 5 * GiB; Require(StorageTextureBudget(memory, &reported, 1 * GiB, 2 * GiB) == 2560 * MiB, "2 GiB used outside the texture caches on a 10 GiB budget lowered the storage image cache below its quarter"); - reported.heapUsage[2] = 8 * GiB; - Require(StorageTextureBudget(memory, &reported, 1 * GiB, 2 * GiB) == 1792 * MiB, "a 10 GiB budget with 5 GiB used outside the texture caches does not leave 1792 MiB after the sampled textures' 2 GiB floor and 1280 MiB of headroom"); + reported.heapUsage[2] = 8 * GiB + 512 * MiB; + Require(StorageTextureBudget(memory, &reported, 1 * GiB, 2 * GiB) == 2304 * MiB, "a 10 GiB budget with 5632 MiB used outside the texture caches does not leave 2304 MiB after 1 GiB of sampled textures and 1280 MiB of headroom"); reported.heapUsage[2] = 9 * GiB; - Require(StorageTextureBudget(memory, &reported, 3 * GiB, 2 * GiB) == 1792 * MiB, "3 GiB of sampled textures were not reserved past their 2 GiB floor"); + Require(StorageTextureBudget(memory, &reported, 3 * GiB, 2 * GiB) == 1792 * MiB, "3 GiB of sampled textures were not reserved in full"); reported.heapUsage[2] = 1 * GiB; Require(StorageTextureBudget(memory, &reported, 1 * GiB, 2 * GiB) == 2560 * MiB, "usage below the cached bytes is not read as no other device memory in use"); Require(StorageTextureBudget(memory, &reported, 7 * GiB, 1 * GiB) == 1792 * MiB, "7 GiB of sampled textures with the usage below the cached bytes do not leave 1792 MiB after their reservation and 1280 MiB of headroom"); @@ -6413,6 +6591,35 @@ void storageTextureBudgetTests() { Require(StorageTextureBudget(memory, &reported, 0, 0) == 512 * MiB, "a 1 GiB budget does not keep the 512 MiB floor"); reported.heapBudget[2] = 40 * GiB; Require(StorageTextureBudget(memory, &reported, 0, 0) == 4 * GiB, "a budget above the heap size raised the storage image cache past a quarter of the heap"); + memory.memoryHeaps[2].size = 8 * GiB; + reported.heapBudget[2] = 7189 * MiB; + reported.heapUsage[2] = 4491 * MiB; + Require(StorageTextureBudget(memory, &reported, 49 * MiB, 506 * MiB) == 7189 * MiB / 4u, "49 MiB of sampled textures on an 8 GiB heap with a 7189 MiB budget and 3936 MiB used outside the texture caches held the storage image cache below a quarter of the budget"); + reported.heapUsage[2] = 6490 * MiB; + Require(StorageTextureBudget(memory, &reported, 2 * GiB, 506 * MiB) == 512 * MiB, "2 GiB of sampled textures on an 8 GiB heap with a 7189 MiB budget and 3936 MiB used outside the texture caches did not take the storage image cache back to its 512 MiB floor"); + memory.memoryHeaps[2].size = 10 * GiB; + reported.heapBudget[2] = 8767 * MiB; + reported.heapUsage[2] = 7981 * MiB; + Require(StorageTextureBudget(memory, &reported, 1427 * MiB, 490 * MiB) == 512 * MiB, "a 10 GiB heap at 7981 of its 8767 MiB budget with 1427 MiB of sampled textures does not keep the storage image cache at its 512 MiB floor"); + for (const auto heapGiB : {2u, 4u, 6u, 8u, 16u, 32u}) { + memory.memoryHeaps[2].size = heapGiB * GiB; + for (std::uint64_t budget = GiB; budget <= heapGiB * GiB; budget += 512 * MiB) { + reported.heapBudget[2] = budget; + for (std::uint64_t sampled = 0; sampled <= budget; sampled += 256 * MiB) { + std::uint64_t previous = 0; + for (std::uint64_t outside = 0; outside <= budget + GiB; outside += 128 * MiB) { + reported.heapUsage[2] = outside + sampled + 512 * MiB; + const auto limit = StorageTextureBudget(memory, &reported, sampled, 512 * MiB); + Require(limit >= 512 * MiB, "pressure sweep lost the storage cache floor"); + Require(limit <= std::max(512 * MiB, budget / 4u), "pressure sweep exceeded a quarter of the reported budget above the floor"); + Require(limit <= std::max(2 * GiB, heapGiB * GiB / 4u), "pressure sweep exceeded the heap storage cache cap"); + if (outside != 0) Require(limit <= previous, "additional non-cache memory usage grew the storage cache budget"); + if (limit > 512 * MiB) Require(outside + sampled + budget / 8u + limit <= budget, "storage cache budget consumed live sampled memory or pressure headroom"); + previous = limit; + } + } + } + } memory.memoryHeaps[0].flags = 0; memory.memoryHeaps[2].flags = 0; Require(StorageTextureBudget(memory, &reported, 0, 0) == 2 * GiB, "a device without a device-local heap does not keep 2 GiB of storage images"); @@ -6890,6 +7097,412 @@ void sampleLocationGridFallbackTests() { mock = MockVulkan{}; } +std::string varyingIndex(std::span code) { + Require(code.size() > 5 && code[0] == spv::MagicNumber, "a sample order shader is not a SPIR-V module"); + std::set constants; + for (std::size_t at = 5; at < code.size();) { + const auto count = code[at] >> 16u; + const auto opcode = static_cast(code[at] & 0xffffu); + Require(count != 0 && at + count <= code.size(), "a sample order shader holds a malformed instruction"); + if (opcode == spv::OpConstant) constants.insert(code[at + 2]); + if (opcode == spv::OpVectorExtractDynamic || opcode == spv::OpVectorInsertDynamic) return "a vector component chosen per invocation (opcode " + std::to_string(opcode) + ")"; + if (opcode == spv::OpAccessChain || opcode == spv::OpInBoundsAccessChain || opcode == spv::OpPtrAccessChain || opcode == spv::OpInBoundsPtrAccessChain) { + for (std::size_t operand = 4; operand < count; ++operand) { + if (!constants.contains(code[at + operand])) return "an access chain into %" + std::to_string(code[at + 3]) + " through the non-constant index %" + std::to_string(code[at + operand]); + } + } + at += count; + } + return {}; +} + +VkSampleCountFlags orderStorageCounts = VK_SAMPLE_COUNT_2_BIT | VK_SAMPLE_COUNT_4_BIT; +bool orderSixteenBitStencil = true; +std::vector pipelineSampleMasks; + +VKAPI_ATTR VkResult VKAPI_CALL sampleOrderImageProperties(VkPhysicalDevice, VkFormat, VkImageType, VkImageTiling, VkImageUsageFlags usage, VkImageCreateFlags, VkImageFormatProperties* properties) { + *properties = {}; + properties->maxExtent = {16384, 16384, 1}; + properties->maxMipLevels = 1; + properties->maxArrayLayers = 1; + properties->sampleCounts = (usage & VK_IMAGE_USAGE_STORAGE_BIT) != 0 ? orderStorageCounts : VK_SAMPLE_COUNT_1_BIT | VK_SAMPLE_COUNT_2_BIT | VK_SAMPLE_COUNT_4_BIT; + return VK_SUCCESS; +} + +VKAPI_ATTR void VKAPI_CALL sampleOrderFormatProperties(VkPhysicalDevice, VkFormat format, VkFormatProperties* properties) { + *properties = {}; + if (format == VK_FORMAT_D16_UNORM_S8_UINT && !orderSixteenBitStencil) return; + properties->optimalTilingFeatures = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT | VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT | VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT; +} + +VKAPI_ATTR VkResult VKAPI_CALL sampleOrderCreateGraphicsPipelines(VkDevice device, VkPipelineCache cache, std::uint32_t count, const VkGraphicsPipelineCreateInfo* infos, const VkAllocationCallbacks* allocator, VkPipeline* pipelines) { + const auto* samples = infos[0].pMultisampleState; + pipelineSampleMasks.push_back(samples != nullptr && samples->pSampleMask != nullptr ? *samples->pSampleMask : ~VkSampleMask{0}); + return sampleLocationCreateGraphicsPipelines(device, cache, count, infos, allocator, pipelines); +} + +PFN_vkVoidFunction VKAPI_CALL sampleOrderProc(VkDevice device, const char* name) { + if (std::string_view(name) == "vkCreateGraphicsPipelines") return reinterpret_cast(sampleOrderCreateGraphicsPipelines); + return sampleLocationProc(device, name); +} + +struct DepthOrderEvent { + bool copy = false; + VkImage image = VK_NULL_HANDLE; + VkImageLayout from = VK_IMAGE_LAYOUT_UNDEFINED; + VkImageLayout to = VK_IMAGE_LAYOUT_UNDEFINED; + VkImageAspectFlags aspects = 0; + VkExtent2D grid{}; + std::vector locations; +}; + +std::vector depthOrderEvents; + +VKAPI_ATTR VkResult VKAPI_CALL depthOrderCreateImage(VkDevice, const VkImageCreateInfo*, const VkAllocationCallbacks*, VkImage* image) { + *image = makeHandle(); + ++mock.live; + return VK_SUCCESS; +} + +VKAPI_ATTR void VKAPI_CALL depthOrderDestroyImage(VkDevice, VkImage, const VkAllocationCallbacks*) { --mock.live; } +VKAPI_ATTR void VKAPI_CALL depthOrderImageRequirements(VkDevice, VkImage, VkMemoryRequirements* requirements) { *requirements = {256, 1, 1}; } +VKAPI_ATTR VkResult VKAPI_CALL depthOrderBindImage(VkDevice, VkImage, VkDeviceMemory, VkDeviceSize) { return VK_SUCCESS; } + +VKAPI_ATTR VkResult VKAPI_CALL depthOrderCreateView(VkDevice, const VkImageViewCreateInfo*, const VkAllocationCallbacks*, VkImageView* view) { + *view = makeHandle(); + ++mock.live; + return VK_SUCCESS; +} + +VKAPI_ATTR void VKAPI_CALL depthOrderDestroyView(VkDevice, VkImageView, const VkAllocationCallbacks*) { --mock.live; } + +VKAPI_ATTR void VKAPI_CALL depthOrderBarrier(VkCommandBuffer, VkPipelineStageFlags, VkPipelineStageFlags, VkDependencyFlags, std::uint32_t, const VkMemoryBarrier*, std::uint32_t, const VkBufferMemoryBarrier*, std::uint32_t count, const VkImageMemoryBarrier* barriers) { + for (std::uint32_t index = 0; index < count; ++index) { + const auto& barrier = barriers[index]; + DepthOrderEvent event{false, barrier.image, barrier.oldLayout, barrier.newLayout, barrier.subresourceRange.aspectMask, {}, {}}; + const auto* locations = static_cast(barrier.pNext); + if (locations != nullptr && locations->sType == VK_STRUCTURE_TYPE_SAMPLE_LOCATIONS_INFO_EXT) { + event.grid = locations->sampleLocationGridSize; + event.locations.assign(locations->pSampleLocations, locations->pSampleLocations + locations->sampleLocationsCount); + } + depthOrderEvents.push_back(event); + } +} + +VKAPI_ATTR void VKAPI_CALL depthOrderCopy(VkCommandBuffer, VkImage source, VkImageLayout layout, VkImage, VkImageLayout, std::uint32_t, const VkImageCopy*) { + depthOrderEvents.push_back({true, source, layout, layout, 0, {}, {}}); +} + +VKAPI_ATTR void VKAPI_CALL depthOrderEndPass(VkCommandBuffer) {} +VKAPI_ATTR void VKAPI_CALL depthOrderDraw(VkCommandBuffer, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t) {} +VKAPI_ATTR void VKAPI_CALL depthOrderStencil(VkCommandBuffer, VkStencilFaceFlags, std::uint32_t) {} + +PFN_vkVoidFunction VKAPI_CALL depthOrderProc(VkDevice device, const char* name) { + static const std::map table{ + {"vkCreateImage", reinterpret_cast(depthOrderCreateImage)}, + {"vkDestroyImage", reinterpret_cast(depthOrderDestroyImage)}, + {"vkGetImageMemoryRequirements", reinterpret_cast(depthOrderImageRequirements)}, + {"vkBindImageMemory", reinterpret_cast(depthOrderBindImage)}, + {"vkCreateImageView", reinterpret_cast(depthOrderCreateView)}, + {"vkDestroyImageView", reinterpret_cast(depthOrderDestroyView)}, + {"vkCmdPipelineBarrier", reinterpret_cast(depthOrderBarrier)}, + {"vkCmdCopyImage", reinterpret_cast(depthOrderCopy)}, + {"vkCmdEndRenderPass", reinterpret_cast(depthOrderEndPass)}, + {"vkCmdDraw", reinterpret_cast(depthOrderDraw)}, + {"vkCmdSetStencilWriteMask", reinterpret_cast(depthOrderStencil)}, + {"vkCmdSetStencilReference", reinterpret_cast(depthOrderStencil)} + }; + const auto it = table.find(name); + return it != table.end() ? it->second : sampleLocationProc(device, name); +} + +void sampleOrderTests() { + using namespace AgcDriver::Graphics; + auto queue = makeState(); + queue.context[0x2f8] = 0x00100001; + queue.context[0x292] = 3; + queue.context[0x3b8] = 0x0dc00000u; + queue.context[0x31d] = 0x9000; + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + const auto decode = [&](std::array words) { + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = words[pixel]; + return DecodeState(queue); + }; + const auto village = decode({0x0c04u, 0x0c04u, 0x040cu, 0x040cu}); + const auto residentEvil4 = decode({0x040cu, 0x040cu, 0x0c04u, 0x0c04u}); + const auto uniform = decode({0x0c04u, 0x0c04u, 0x0c04u, 0x0c04u}); + const auto moved = decode({0x0c04u, 0x0c04u, 0x040bu, 0x040bu}); + const SampleOrder rowSwap{KeptSampleOrder, KeptSampleOrder, 0x76543201u, 0x76543201u}; + Require(SameSetSampleOrder(village) == rowSwap, "the 2x checkerboard (PPSA01556) did not reorder only its odd rows' two samples"); + Require(SameSetSampleOrder(residentEvil4) == rowSwap, "the 2x checkerboard with its rows swapped (PPSA07411) did not reorder only its odd rows' two samples"); + Require(!SameSetSampleOrder(uniform) && !SameSetSampleOrder(moved), "a uniform pattern or one whose odd rows hold other positions was taken as a reordering of one set"); + State four{}; + four.samples = 4; + four.customSampleLocations = true; + four.sampleLocationGrid = true; + const std::array positions{0x2eu, 0xe6u, 0x2au, 0x62u}; + const std::array, 4> placed{{{0, 1, 2, 3}, {1, 0, 3, 2}, {2, 3, 1, 0}, {3, 2, 0, 1}}}; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) { + for (std::uint32_t sample = 0; sample < 4; ++sample) four.sampleLocations[pixel * 8u + placed[pixel][sample]] = positions[sample]; + } + const SampleOrder fourOrder{KeptSampleOrder, 0x76542301u, 0x76540132u, 0x76541023u}; + Require(SameSetSampleOrder(four) == fourOrder, "a 4x pattern whose pixels hold one set of positions in four orders did not give each pixel its order"); + Require(InvertSampleOrder(0x76543201u) == 0x76543201u && InvertSampleOrder(0x76540132u) == 0x76541023u && InvertSampleOrder(KeptSampleOrder) == KeptSampleOrder, "inverting a sample order did not undo it"); + Require(HostSampleBits(0x1u, 0x76543201u) == 0x2u && HostSampleBits(0x1u, 0x76540132u) == 0x8u && HostSampleBits(0xffffffffu, 0x76540132u) == 0xffffffffu, "a guest sample mask did not move to the host samples at its guest samples' locations"); + Require(ReordersSamples(rowSwap) && !ReordersSamples(SampleOrder{}) && !ReordersSamples(SampleOrder{KeptSampleOrder, KeptSampleOrder, KeptSampleOrder, KeptSampleOrder}), "a sample order that keeps every index counted as a reordering"); + Require(!HostsSampleLocationGrid({0, 0}) && !HostsSampleLocationGrid({1, 1}) && !HostsSampleLocationGrid({2, 1}) && !HostsSampleLocationGrid({3, 2}) && HostsSampleLocationGrid({2, 2}) && HostsSampleLocationGrid({4, 2}), "a device's largest sample location grid was taken for one that holds a 2x2 grid, or not, wrongly"); + std::size_t module = 0; + for (const auto code : SampleOrderShaderModules()) { + const auto varying = varyingIndex(code); + Require(varying.empty(), "sample order shader " + std::to_string(module) + " picks a pixel's order word or a held sample through " + varying); + ++module; + } + + const auto capable = [] { + auto context = mockContext(); + context.physical = makeHandle(); + context.device = makeHandle(); + context.deviceProc = sampleOrderProc; + context.imageFormatProperties = sampleOrderImageProperties; + context.formatProperties = sampleOrderFormatProperties; + context.sampleLocationSampleCounts = VK_SAMPLE_COUNT_1_BIT | VK_SAMPLE_COUNT_2_BIT | VK_SAMPLE_COUNT_4_BIT; + context.sampleLocationGridSampleCounts = 0; + context.storageImageMultisample = true; + context.sampleRateShading = true; + context.shaderStencilExport = true; + context.limits.framebufferNoAttachmentsSampleCounts = VK_SAMPLE_COUNT_1_BIT | VK_SAMPLE_COUNT_2_BIT | VK_SAMPLE_COUNT_4_BIT; + context.limits.framebufferDepthSampleCounts = VK_SAMPLE_COUNT_2_BIT | VK_SAMPLE_COUNT_4_BIT; + context.limits.framebufferColorSampleCounts = VK_SAMPLE_COUNT_2_BIT | VK_SAMPLE_COUNT_4_BIT; + context.limits.maxColorAttachments = 8; + context.limits.maxFramebufferWidth = 64; + context.limits.maxFramebufferHeight = 64; + context.limits.maxFramebufferLayers = 1; + return context; + }; + const auto attached = [](const State& decoded, VkFormat depthFormat, bool stencil) { + State state = decoded; + ColorTarget color{}; + color.address = 0x100000u; + color.extent = {64, 64}; + color.format = VK_FORMAT_R16G16B16A16_SFLOAT; + color.elementBytes = 8; + color.samples = state.samples; + state.colors = {color}; + state.color = color; + state.hasColorTarget = true; + VkPipelineColorBlendAttachmentState blend{}; + blend.colorWriteMask = 0xf; + state.blends = {blend}; + state.renderExtent = {64, 64}; + if (depthFormat != VK_FORMAT_UNDEFINED) { + state.depth = DepthTarget{0x200000u, stencil ? 0x300000u : 0u, {64, 64}, depthFormat, 1.0f, 0}; + state.depth->samples = state.samples; + } + return state; + }; + const auto withStencil = attached(village, VK_FORMAT_D32_SFLOAT_S8_UINT, true); + { + const auto context = capable(); + const auto host = HostSampleLocationsFor(context, withStencil); + Require(host.grid.width == 1 && host.grid.height == 1 && host.order == rowSwap, "a 2x checkerboard on a device without a 2x2 grid that can reorder samples did not take the stand-in with its odd rows reordered"); + auto gridded = context; + gridded.sampleLocationGridSampleCounts = VK_SAMPLE_COUNT_2_BIT; + const auto native = HostSampleLocationsFor(gridded, withStencil); + Require(native.grid.width == 2 && native.grid.height == 2 && !ReordersSamples(native.order), "a 2x checkerboard on a device with a 2x2 grid was reordered"); + Require(!ReordersSamples(HostSampleLocationsFor(context, attached(uniform, VK_FORMAT_D32_SFLOAT, false)).order) && !ReordersSamples(HostSampleLocationsFor(context, attached(moved, VK_FORMAT_D32_SFLOAT, false)).order), "a uniform pattern, or one whose pixels hold other positions, was reordered"); + const auto refused = [&](Context device, const State& state, std::string_view reason, const std::string& what) { + Require(!ReordersSamples(HostSampleLocationsFor(device, state).order), what + " was reordered"); + Require(SampleOrderRefusal(device, state).find(reason) != std::string::npos, what + " was not refused for " + std::string(reason) + ": " + SampleOrderRefusal(device, state)); + }; + auto noStorage = context; + noStorage.storageImageMultisample = false; + auto depthOnly = withStencil; + depthOnly.colors.clear(); + depthOnly.blends.clear(); + depthOnly.hasColorTarget = false; + Require(ReordersSamples(HostSampleLocationsFor(context, depthOnly).order), "a depth-only pass on a device with multisampled storage images was not reordered"); + refused(noStorage, depthOnly, "lacks shaderStorageImageMultisample", "a depth-only pass on a device without multisampled storage images"); + Require(HostSampleLocationsFor(noStorage, depthOnly).order == HostSampleLocationsFor(noStorage, withStencil).order, "a depth-only pass and a color pass on one depth target took different sample orders on a device without multisampled storage images"); + refused(noStorage, withStencil, "lacks shaderStorageImageMultisample", "a color target on a device without multisampled storage images"); + auto noSampleShading = context; + noSampleShading.sampleRateShading = false; + refused(noSampleShading, withStencil, "sampleRateShading", "a device without sample-rate shading"); + orderStorageCounts = VK_SAMPLE_COUNT_4_BIT; + refused(context, withStencil, "has no storage view at 2 samples", "a color format without 2x storage"); + orderStorageCounts = VK_SAMPLE_COUNT_2_BIT | VK_SAMPLE_COUNT_4_BIT; + auto twentyFour = context; + twentyFour.physical = makeHandle(); + orderSixteenBitStencil = false; + refused(twentyFour, attached(village, VK_FORMAT_D16_UNORM_S8_UINT, true), "is not written back exactly", "16-bit depth with stencil kept as 24-bit depth"); + orderSixteenBitStencil = true; + auto sixteen = context; + sixteen.physical = makeHandle(); + Require(ReordersSamples(HostSampleLocationsFor(sixteen, attached(village, VK_FORMAT_D16_UNORM_S8_UINT, true)).order), "16-bit depth with stencil kept as 16-bit depth was not reordered"); + + using Method = SampleOrderMethod; + const auto steps = [](const std::vector& plan, std::initializer_list expected, const std::string& what) { + Require(plan.size() == expected.size(), what + " planned " + std::to_string(plan.size()) + " attachments"); + std::size_t index = 0; + for (const auto& step : expected) { + const auto& planned = plan[index++]; + Require(planned.attachment == step.attachment && planned.method == step.method && planned.toHost == step.toHost && planned.toGuest == step.toGuest, what + " planned attachment " + std::to_string(planned.attachment) + " differently"); + } + }; + steps(PlanSampleOrder(context, withStencil, rowSwap), {{0, Method::Storage, true, true}, {1, Method::DepthStencilExport, true, true}}, "a color and depth-stencil pass with stencil export"); + auto noExport = context; + noExport.shaderStencilExport = false; + steps(PlanSampleOrder(noExport, withStencil, rowSwap), {{0, Method::Storage, true, true}, {1, Method::DepthStencilBits, true, true}}, "a color and depth-stencil pass without stencil export"); + steps(PlanSampleOrder(context, attached(village, VK_FORMAT_D32_SFLOAT, false), rowSwap), {{0, Method::Storage, true, true}, {1, Method::Depth, true, true}}, "a color and depth pass"); + steps(PlanSampleOrder(context, attached(village, VK_FORMAT_D32_SFLOAT_S8_UINT, false), rowSwap), {{0, Method::Storage, true, true}, {1, Method::Depth, true, true}}, "a depth target whose host format has stencil but the guest's has none"); + steps(PlanSampleOrder(context, withStencil, rowSwap, VK_ATTACHMENT_LOAD_OP_CLEAR, VK_ATTACHMENT_STORE_OP_STORE), {{0, Method::Storage, false, true}, {1, Method::DepthStencilExport, false, true}}, "a pass that clears its attachments"); + steps(PlanSampleOrder(context, withStencil, rowSwap, VK_ATTACHMENT_LOAD_OP_LOAD, VK_ATTACHMENT_STORE_OP_DONT_CARE), {{0, Method::Storage, true, false}, {1, Method::DepthStencilExport, true, false}}, "a pass that discards its attachments"); + steps(PlanSampleOrder(context, withStencil, rowSwap, VK_ATTACHMENT_LOAD_OP_DONT_CARE, VK_ATTACHMENT_STORE_OP_DONT_CARE), {}, "a pass that neither loads nor stores"); + steps(PlanSampleOrder(context, withStencil, SampleOrder{}), {}, "a pass that keeps the guest's order"); + auto single = withStencil; + single.samples = 1; + single.colors[0].samples = 1; + single.depth->samples = 1; + steps(PlanSampleOrder(context, single, rowSwap), {}, "a single-sampled pass"); + } + + auto masked = attached(village, VK_FORMAT_UNDEFINED, false); + masked.quadSampleMask = {1, 1, 1, 1}; + Require(SampleMaskFollowsOrder(masked, rowSwap) && QuadSampleMasks(masked, rowSwap) == std::array{1, 1, 1, 1} && QuadSampleMasks(masked) == std::array{0xffffu, 0xffffu, 0xffffu, 0xffffu}, "a uniform mask of sample 0 did not move to the pixel shader under the checkerboard's order"); + auto empty = masked; + empty.quadSampleMask = {0, 0, 0, 0}; + Require(!SampleMaskFollowsOrder(empty, rowSwap) && !SampleMaskFollowsOrder(masked, SampleOrder{}), "an empty mask, or a mask without a reordering, moved to the pixel shader"); + { + auto earlyTested = attached(village, VK_FORMAT_D32_SFLOAT, false); + earlyTested.quadSampleMask = {1, 1, 1, 1}; + earlyTested.pixelEarlyZ = true; + earlyTested.depthTest = true; + ShaderRecompiler::RecompileResult memoryWriter, memoryKeeper; + memoryWriter.spirv = makeModule({.fragment = true}); + memoryKeeper.spirv = memoryWriter.spirv; + ShaderRecompiler::DescriptorBinding written{}; + written.kind = ShaderRecompiler::DescriptorKind::StorageBuffer; + written.role = ShaderRecompiler::DescriptorRole::GuestBuffers; + written.count = 1; + written.bufferWritten = {true}; + memoryWriter.bindings.push_back(written); + const std::array writing{{{ShaderRecompiler::ShaderStage::Fragment, &memoryWriter, 0}}}; + const std::array notWriting{{{ShaderRecompiler::ShaderStage::Fragment, &memoryKeeper, 0}}}; + const auto context = capable(); + Require(SampleMaskFollowsOrder(earlyTested, HostSampleLocationsFor(context, earlyTested).order), "a uniform mask of sample 0 with a depth target did not move to the pixel shader under the checkerboard's order"); + expectFailure([&] { ValidateSampleMask(context, earlyTested, writing); }, "from a pixel program with early depth or stencil tests that writes memory, is unsupported"); + ValidateSampleMask(context, earlyTested, notWriting); + auto gridded = context; + gridded.sampleLocationGridSampleCounts = VK_SAMPLE_COUNT_2_BIT; + ValidateSampleMask(gridded, earlyTested, writing); + } + + ShaderRecompiler::RecompileResult vertex, fragment; + vertex.spirv = makeModule({}); + fragment.spirv = makeModule({.fragment = true}); + vertex.variantId = 1; + fragment.variantId = 2; + const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, PipelinePushSlotBytes}}}; + const std::array mainKey{41}; + const std::array pixelZero{{{0.75f, 0.5f}, {0.25f, 0.5f}}}; + for (const bool libraries : {false, true}) { + for (const bool maskedPass : {false, true}) { + mock = MockVulkan{}; + pipelineSampleLocations.clear(); + passSampleLocations.clear(); + pipelineSampleMasks.clear(); + auto context = capable(); + context.graphicsPipelineLibrary = libraries; + context.pipelineLinkTimeOptimization = false; + auto state = maskedPass ? masked : attached(village, VK_FORMAT_UNDEFINED, false); + const PipelineSetLayouts resources{makeHandle(), VK_NULL_HANDLE, false, mainKey, {}}; + std::size_t reorderedMasks = 0; + { + Pipeline pipeline(context, state, VertexInputLayout{}, resources, shaders, VK_IMAGE_LAYOUT_GENERAL); + Require(pipeline.ReordersSamples() && pipeline.HostSampleOrder() == rowSwap, "a 2x checkerboard pipeline on a device without a 2x2 grid did not reorder its odd rows"); + reorderedMasks = pipelineSampleMasks.size(); + auto gridded = context; + gridded.sampleLocationGridSampleCounts = VK_SAMPLE_COUNT_2_BIT; + Pipeline native(gridded, state, VertexInputLayout{}, resources, shaders, VK_IMAGE_LAYOUT_GENERAL); + Require(!native.ReordersSamples(), "a 2x checkerboard pipeline on a device with a 2x2 grid reordered its samples"); + } + ClearPipelineLibraries(context.device); + Require(mock.live == 0, "sample order pipeline tests leaked Vulkan objects"); + Require(!pipelineSampleLocations.empty(), "the reordered pipeline took no sample locations"); + const auto& standIn = pipelineSampleLocations.front(); + Require(standIn.grid.width == 1 && standIn.grid.height == 1 && standIn.locations.size() == 2 && standIn.locations[0].x == pixelZero[0].x && standIn.locations[1].x == pixelZero[1].x, "the reordered pipeline did not draw with pixel (0,0)'s locations"); + const auto keepsMask = [&](std::size_t begin, std::size_t end) { return std::any_of(pipelineSampleMasks.begin() + static_cast(begin), pipelineSampleMasks.begin() + static_cast(end), [](VkSampleMask mask) { return mask == 1u; }); }; + Require(reorderedMasks != 0 && !keepsMask(0, reorderedMasks), "the reordered pipeline kept a static sample mask its odd rows would apply to the other sample"); + Require(keepsMask(reorderedMasks, pipelineSampleMasks.size()) == maskedPass, "the pipeline on a device with a 2x2 grid did not keep the static sample mask exactly when the pass has one"); + } + } + mock = MockVulkan{}; + + { + auto context = capable(); + context.deviceProc = depthOrderProc; + context.memory.memoryTypes[0].propertyFlags |= VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; + const auto commands = makeHandle(); + const auto depthImage = makeHandle(); + const SampleOrderTarget target{depthImage, makeHandle(), VK_FORMAT_D32_SFLOAT_S8_UINT, {16, 8}, 2, SampleOrderMethod::Depth, nullptr}; + const auto onTarget = [&] { + std::vector events; + for (const auto& event : depthOrderEvents) { + if (event.image == depthImage) events.push_back(event); + } + return events; + }; + const auto located = [](const DepthOrderEvent& event, VkImageLayout from, VkImageLayout to, VkImageAspectFlags aspects, VkExtent2D grid, std::span locations) { + if (event.copy || event.from != from || event.to != to || event.aspects != aspects || event.grid.width != grid.width || event.grid.height != grid.height || event.locations.size() != locations.size()) return false; + for (std::size_t index = 0; index < locations.size(); ++index) { + if (event.locations[index].x != locations[index].x || event.locations[index].y != locations[index].y) return false; + } + return true; + }; + constexpr VkImageAspectFlags depthStencil = VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT; + std::vector> kept; + for (const bool standIn : {true, false}) { + depthOrderEvents.clear(); + SampleOrderPass pass{rowSwap, {16, 8}, {}, standIn}; + std::copy(pixelZero.begin(), pixelZero.end(), pass.locations.begin()); + RecordSampleOrder(context, commands, pass, std::span(&target, 1u), false, kept); + const auto events = onTarget(); + if (standIn) { + Require(events.size() == 3 && located(events[0], VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, depthStencil, {1, 1}, pixelZero) && events[1].copy && events[1].from == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL && located(events[2], VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL, depthStencil, {1, 1}, pixelZero), "the depth swap did not copy its target between transitions that name the stand-in sample locations it was rendered with"); + } else { + Require(events.size() == 1 && events[0].copy && events[0].from == VK_IMAGE_LAYOUT_GENERAL, "the depth swap of a pass without sample locations transitioned its target"); + } + } + depthOrderEvents.clear(); + DepthSampleLocations depth; + depth.image = depthImage; + depth.format = VK_FORMAT_D32_SFLOAT; + depth.samples = 2; + depth.grid = {2, 2}; + const std::array grid{{{0.75f, 0.5f}, {0.25f, 0.5f}, {0.75f, 0.5f}, {0.25f, 0.5f}, {0.25f, 0.5f}, {0.75f, 0.5f}, {0.25f, 0.5f}, {0.75f, 0.5f}}}; + std::copy(grid.begin(), grid.end(), depth.locations.begin()); + RecordDepthSampleLocations(context, commands, depth); + const auto events = onTarget(); + Require(events.size() == 2 && located(events[0], VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_ASPECT_DEPTH_BIT, {2, 2}, grid) && located(events[1], VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_ASPECT_DEPTH_BIT, {2, 2}, grid), "a depth target drawn on a 2x2 sample location grid was not left in the general layout through transitions that name that grid's locations"); + kept.clear(); + ClearSampleOrders(context.device); + Require(mock.live == 0, "the depth swap tests leaked Vulkan objects"); + depthOrderEvents.clear(); + } + mock = MockVulkan{}; + + ShaderRecompiler::RecompileRequest request{}; + request.shader = {ShaderRecompiler::ShaderStage::Fragment, 0x20000u, {}, 0, {}}; + request.context.waveSize = 64; + request.context.pixel = ShaderRecompiler::ShaderPixelStageInfo{}; + const auto keptHash = ShaderRecompiler::RecompileCacheKey::ContextHash(request); + request.context.pixel->sampleOrder = rowSwap; + Require(ShaderRecompiler::RecompileCacheKey::ContextHash(request) != keptHash, "the pixel shader's sample order did not key its variant"); + const ShaderRecompiler::RequestSerializer serializer; + const auto back = serializer.Deserialize(serializer.Serialize(request)); + Require(back.request.context.pixel.has_value() && back.request.context.pixel->sampleOrder == rowSwap, "the recompile request lost the pixel shader's sample order"); +} + void pipelineLibrarySetTests() { using namespace AgcDriver::Graphics; for (const auto mask : {0u, 1u, 2u, 3u}) { @@ -7170,7 +7783,7 @@ void barycentricEmulationTests() { } return values; }; - auto patches = BuildBarycentricTessellationShaders(vertex, fragment, tessellationTarget, 128u); + auto patches = BuildBarycentricTessellationShaders(vertex, fragment, tessellationTarget, 128u, 0u); Require(modes(patches.control) == std::vector{spv::ExecutionModeOutputVertices, spv::ExecutionModeTriangles, spv::ExecutionModeSpacingEqual, spv::ExecutionModeVertexOrderCw}, "the generated control stage does not declare three output vertices and the triangle domain"); Require(modes(patches.evaluation) == std::vector{spv::ExecutionModeTriangles, spv::ExecutionModeSpacingEqual, spv::ExecutionModeVertexOrderCw}, "the generated evaluation stage does not declare the triangle domain"); const std::array patchShaders{{{ShaderStage::Vertex, &vertex, 0}, {ShaderStage::TessellationControl, &patches.control, 0}, {ShaderStage::TessellationEvaluation, &patches.evaluation, 0}, {ShaderStage::Fragment, &fragment, 0}}}; @@ -7184,27 +7797,41 @@ void barycentricEmulationTests() { state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; const std::array plainPatchShaders{{{ShaderStage::Vertex, &vertex, 0}, {ShaderStage::TessellationControl, &patches.control, 0}, {ShaderStage::TessellationEvaluation, &patches.evaluation, 0}, {ShaderStage::Fragment, &plain, 0}}}; expectFailure([&] { ValidateShaders(plainPatchShaders, state, subgroup, false); }, "stage count"); - expectFailure([&] { static_cast(BuildBarycentricTessellationShaders(vertex, plain, tessellationTarget, 128u)); }, "does not emulate barycentrics"); + expectFailure([&] { static_cast(BuildBarycentricTessellationShaders(vertex, plain, tessellationTarget, 128u, 0u)); }, "does not emulate barycentrics"); auto noTessellation = tessellationTarget; noTessellation.tessellation.reset(); - expectFailure([&] { static_cast(BuildBarycentricTessellationShaders(vertex, fragment, noTessellation, 128u)); }, "tessellation shaders are unavailable"); - expectFailure([&] { static_cast(BuildBarycentricTessellationShaders(vertex, fragment, target, 128u)); }, "tessellation shaders are unavailable"); + expectFailure([&] { static_cast(BuildBarycentricTessellationShaders(vertex, fragment, noTessellation, 128u, 0u)); }, "tessellation shaders are unavailable"); + expectFailure([&] { static_cast(BuildBarycentricTessellationShaders(vertex, fragment, target, 128u, 0u)); }, "tessellation shaders are unavailable"); auto noCapability = tessellationTarget; noCapability.supportedCapabilities = std::span(capabilities).first(1); - expectFailure([&] { static_cast(BuildBarycentricTessellationShaders(vertex, fragment, noCapability, 128u)); }, "tessellation capability is unavailable"); + expectFailure([&] { static_cast(BuildBarycentricTessellationShaders(vertex, fragment, noCapability, 128u, 0u)); }, "tessellation capability is unavailable"); auto smallPatches = tessellationTarget; smallPatches.tessellation->maxEvaluationOutputComponents = 4u; - expectFailure([&] { static_cast(BuildBarycentricTessellationShaders(vertex, fragment, smallPatches, 128u)); }, "exceeds device limits"); - expectFailure([&] { static_cast(BuildBarycentricTessellationShaders(vertex, fragment, tessellationTarget, 0u)); }, "exceeds device limits"); + expectFailure([&] { static_cast(BuildBarycentricTessellationShaders(vertex, fragment, smallPatches, 128u, 0u)); }, "exceeds device limits"); + expectFailure([&] { static_cast(BuildBarycentricTessellationShaders(vertex, fragment, tessellationTarget, 0u, 0u)); }, "exceeds device limits"); + expectFailure([&] { static_cast(BuildBarycentricTessellationShaders(vertex, fragment, tessellationTarget, 128u, 1u)); }, "first or the last"); + static_cast(BuildBarycentricTessellationShaders(vertex, fragment, tessellationTarget, 128u, 2u)); + { + auto context = mockContext(); + context.provokingVertexLast = false; + auto lastState = state; + lastState.provokingVertexMode = VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT; + ValidateProvokingVertex(context, lastState, patchShaders); + expectFailure([&] { ValidateProvokingVertex(context, lastState, shaders); }, "last provoking vertex requires VK_EXT_provoking_vertex"); + RecompileResult flatVertices = fragment; + for (auto& parameter : flatVertices.fragmentParameters) parameter.flat = true; + const std::array flatPatchShaders{{{ShaderStage::Vertex, &vertex, 0}, {ShaderStage::TessellationControl, &patches.control, 0}, {ShaderStage::TessellationEvaluation, &patches.evaluation, 0}, {ShaderStage::Fragment, &flatVertices, 0}}}; + expectFailure([&] { ValidateProvokingVertex(context, lastState, flatPatchShaders); }, "explicit per-vertex flat interpolation"); + } RecompileResult clipped; clipped.spirv = makeModule({.parameterOutput = true, .clipDistanceLength = 2}); clipped.parameterExports = {0u}; - expectFailure([&] { static_cast(BuildBarycentricTessellationShaders(clipped, fragment, tessellationTarget, 128u)); }, "need shaderClipDistance"); + expectFailure([&] { static_cast(BuildBarycentricTessellationShaders(clipped, fragment, tessellationTarget, 128u, 0u)); }, "need shaderClipDistance"); const std::array clipCapabilities{spv::CapabilityShader, spv::CapabilityTessellation, spv::CapabilityClipDistance}; auto clipTarget = tessellationTarget; clipTarget.supportedCapabilities = clipCapabilities; - auto clippedPatches = BuildBarycentricTessellationShaders(clipped, fragment, clipTarget, 128u); + auto clippedPatches = BuildBarycentricTessellationShaders(clipped, fragment, clipTarget, 128u, 0u); const auto memberBuiltIn = [](const RecompileResult& result, spv::BuiltIn builtin) { const auto& words = result.spirv.Words(); for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { @@ -7229,7 +7856,7 @@ void barycentricEmulationTests() { const std::array clippedShaders{{{ShaderStage::Vertex, &clipped, 0}, {ShaderStage::TessellationControl, &clippedPatches.control, 0}, {ShaderStage::TessellationEvaluation, &clippedPatches.evaluation, 0}, {ShaderStage::Fragment, &fragment, 0}}}; ValidateShaders(clippedShaders, state, subgroup, false, false, false, false, false, false, true); vertex.parameterExports.clear(); - expectFailure([&] { static_cast(BuildBarycentricTessellationShaders(vertex, fragment, tessellationTarget, 128u)); }, "not exported by the vertex shader"); + expectFailure([&] { static_cast(BuildBarycentricTessellationShaders(vertex, fragment, tessellationTarget, 128u, 0u)); }, "not exported by the vertex shader"); expectFailure([&] { static_cast(BuildBarycentricGeometryShader(vertex, fragment, target, limits)); }, "not exported by the vertex shader"); } @@ -7948,12 +8575,52 @@ void stencilRefExportTests() { Require(!AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck accepted a stencil reference export with Z format " + std::to_string(zFormat)); } } - for (const auto shaderControl : {0x54u, 0x56u}) { - const auto queue = stencilRefExportQueue(shaderControl, 2u); - expectFailure([&] { static_cast(AgcDriver::Graphics::DecodeState(queue)); }, "depth export, shader coverage or ordered fragment execution"); - Require(!AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck accepted STENCIL_OP_VAL_EXPORT_ENABLE in DB_SHADER_CONTROL " + std::to_string(shaderControl)); + for (const auto shaderControl : {0x54u, 0x56u, 0x45u}) { + for (const auto zFormat : {2u, 9u}) { + const auto unused = stencilRefExportQueue(shaderControl, zFormat); + static_cast(AgcDriver::Graphics::DecodeState(unused)); + Require(AgcDriver::Graphics::DrawRejection(unused, false).empty(), "the precheck rejected STENCIL_OP_VAL_EXPORT_ENABLE without a stencil test in DB_SHADER_CONTROL " + std::to_string(shaderControl)); + Require(AgcDriver::Graphics::DecodePixelStageInfo(unused.context, IdentityExports).stencilRefExportEnable == ((shaderControl & 2u) != 0), "STENCIL_OP_VAL_EXPORT_ENABLE without a stencil test exported the stencil reference"); + const bool replaces = (shaderControl & 6u) == 4u; + for (const auto [ops, writeMask, compare, accepted] : {std::tuple{0x000u, 0xffu, 7u, true}, std::tuple{0x737u, 0xffu, 7u, true}, std::tuple{0x110u, 0xffu, 7u, true}, std::tuple{0x050u, 0x00u, 7u, true}, std::tuple{0x040u, 0xffu, 7u, replaces}, std::tuple{0x40040u, 0xffu, 7u, replaces}, std::tuple{0x404u, 0x0fu, 0u, replaces}, std::tuple{0x040u, 0xffu, 2u, false}, std::tuple{0x340u, 0xffu, 7u, false}, std::tuple{0x042u, 0xffu, 7u, false}, std::tuple{0x050u, 0xffu, 7u, false}, std::tuple{0x600u, 0xffu, 7u, false}, std::tuple{0x008u, 0xffu, 7u, false}, std::tuple{0x900u, 0xffu, 7u, false}, std::tuple{0x5000u, 0xffu, 7u, false}}) { + auto stenciled = stencilRefExportQueue(shaderControl, zFormat); + for (const auto [offset, word] : {std::pair{0x010u, 0x80000180u}, std::pair{0x011u, 0x20000181u}, std::pair{0x012u, 0x00001000u}, std::pair{0x013u, 0x00002000u}, std::pair{0x015u, 0x00002000u}, std::pair{0x007u, 0x003f003fu}, std::pair{0x000u, 0u}, std::pair{0x002u, 0u}, std::pair{0x00au, 0u}, std::pair{0x00bu, 0x3f800000u}, std::pair{0x200u, 0x007007b3u}}) stenciled.context[offset] = word; + stenciled.context[0x10b] = ops; + stenciled.context[0x10c] = 0x0100ff00u | (writeMask << 16u); + stenciled.context[0x10d] = stenciled.context[0x10c]; + stenciled.context[0x200] = 0x000000b3u | (compare << 8u) | (compare << 20u); + const auto name = "DB_SHADER_CONTROL " + std::to_string(shaderControl) + " with DB_STENCIL_CONTROL " + std::to_string(ops) + ", write mask " + std::to_string(writeMask) + " and compare " + std::to_string(compare); + if (accepted) { + const auto state = AgcDriver::Graphics::DecodeState(stenciled); + Require(state.stencilTest && state.depth.has_value(), "the stencil operation value fixture has no stencil test: " + name); + const bool routed = replaces && (ops == 0x040u || ops == 0x40040u || ops == 0x404u); + const auto pixel = AgcDriver::Graphics::DecodePixelStageInfo(stenciled.context, IdentityExports); + Require(pixel.stencilRefExportEnable == (routed || (shaderControl & 2u) != 0) && pixel.stencilOperationValueExport == routed, "the stencil reference export does not follow the stencil operation value: " + name); + if (routed) { + const auto replaced = [&](const VkStencilOpState& face, std::uint32_t faceOps) { + const auto expected = [&](std::uint32_t shift) { return ((faceOps >> shift) & 0xfu) == 4u ? VK_STENCIL_OP_REPLACE : VK_STENCIL_OP_KEEP; }; + return face.failOp == expected(0u) && face.passOp == expected(4u) && face.depthFailOp == expected(8u) && face.writeMask == writeMask; + }; + Require(replaced(state.stencilFront, ops) && replaced(state.stencilBack, ops >> 12u) && !AgcDriver::Graphics::StencilReferenceUnused(state), "REPLACE_OP did not replace with the exported stencil value: " + name); + } + } else { + expectFailure([&] { static_cast(AgcDriver::Graphics::DecodeState(stenciled)); }, "with a pixel shader that exports the stencil"); + } + } + } + for (const auto zFormat : {0u, 1u, 3u, 7u}) { + const auto queue = stencilRefExportQueue(shaderControl, zFormat); + expectFailure([&] { static_cast(AgcDriver::Graphics::DecodeState(queue)); }, "depth export, shader coverage or ordered fragment execution"); + Require(!AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck accepted STENCIL_OP_VAL_EXPORT_ENABLE with Z format " + std::to_string(zFormat)); + } } Require(!AgcDriver::Graphics::DecodePixelStageInfo(stencilRefExportQueue(0x50u, 2u).context, IdentityExports).stencilRefExportEnable, "a pixel stage without STENCIL_TEST_VAL_EXPORT_ENABLE exports the stencil reference"); + { + auto early = stencilRefExportQueue(0x1004u, 2u); + for (const auto [offset, word] : {std::pair{0x011u, 0x20000181u}, std::pair{0x002u, 0u}, std::pair{0x000u, 0u}, std::pair{0x200u, 0x007007b3u}, std::pair{0x10bu, 0x040u}, std::pair{0x10cu, 0x01ffff00u}}) early.context[offset] = word; + expectFailure([&] { static_cast(AgcDriver::Graphics::DecodePixelStageInfo(early.context, IdentityExports)); }, "early tests with a pixel shader that exports the stencil operation value"); + Require(!AgcDriver::Graphics::DecodePixelStageInfo(early.context, IdentityExports, true).stencilRefExportEnable, "a skipped pixel program exported the stencil operation value"); + } Require(AgcDriver::Graphics::DecodePixelStageInfo(stencilRefExportQueue(0x1012u, 2u).context, IdentityExports).earlyZ, "DEPTH_BEFORE_SHADER with a stencil reference export did not get early tests"); const std::array stencilExport{spv::CapabilityShader, spv::CapabilityStencilExportEXT}; const std::array plain{spv::CapabilityShader}; @@ -7984,6 +8651,31 @@ void stencilRefExportTests() { const auto emulated = ShaderRecompiler::Recompile(stencilRefExportRequest(0x52u, plain)); Require(emulated.stencilReferences == std::vector{0u} && emulated.stencilReplaySafe && stencilRefShape(emulated.spirv.Words()).empty(), "a constant stencil reference was not lowered to replay"); Require(emulated.stencilReferenceDword == 0u && emulated.pushConstants.size() == sizeof(std::uint32_t), "a stencil-only shader did not reserve dynamic reference data"); + { + const std::array literalStencil{0x7e0e02ffu, 0x00002a05u, 0xf8001882u, 0x700u, 0xbf810000u}; + const auto readsOperationByte = [](std::span words) { + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + if ((words[at] & 0xffffu) == spv::OpShiftRightLogical) return true; + if ((words[at] & 0xffffu) == spv::OpConstant && (words[at] >> 16u) == 4u && words[at + 3] == 0x2au) return true; + } + return false; + }; + const auto testValue = stencilRefExportRequest(0x52u, plain, literalStencil); + auto operationValue = testValue; + operationValue.context.pixel->stencilOperationValueExport = true; + Require(ShaderRecompiler::Recompile(testValue).stencilReferences == std::vector{0x05u}, "a stencil test value replay did not take the low byte of the MRTZ stencil channel"); + Require(ShaderRecompiler::Recompile(operationValue).stencilReferences == std::vector{0x2au}, "a stencil operation value replay did not take the second byte of the MRTZ stencil channel"); + const auto exportedTest = stencilRefExportRequest(0x52u, stencilExport, literalStencil); + auto exportedOperation = exportedTest; + exportedOperation.context.pixel->stencilOperationValueExport = true; + Require(!readsOperationByte(ShaderRecompiler::Recompile(exportedTest).spirv.Words()) && readsOperationByte(ShaderRecompiler::Recompile(exportedOperation).spirv.Words()), "the exported stencil operation value was not taken from the second byte of the MRTZ stencil channel"); + Require(serializer.Deserialize(serializer.Serialize(exportedOperation)).request.context.pixel->stencilOperationValueExport, "the serialized request lost the stencil operation value export"); + std::vector testKey; + std::vector operationKey; + ShaderRecompiler::RecompileCacheKey::Build(exportedTest, testKey); + ShaderRecompiler::RecompileCacheKey::Build(exportedOperation, operationKey); + Require(testKey != operationKey, "the recompile cache key ignores the stencil operation value export"); + } auto replayRequest = stencilRefExportRequest(0x52u, plain); const auto replayHandle = ShaderRecompiler::PrepareShader(replayRequest); const auto replayCapture = ShaderRecompiler::CaptureResources(replayRequest, ShaderRecompiler::SrtRuntime{}, *replayHandle); @@ -8265,6 +8957,25 @@ void LineStripTests() { Require(rejection.empty(), "the draw precheck rejected an indexed line strip with primitive restart: " + rejection); } +std::uint32_t keyedGenerator = 0; +std::size_t keyedWords = 0; + +VKAPI_ATTR VkResult VKAPI_CALL keyedCreateShaderModule(VkDevice, const VkShaderModuleCreateInfo* info, const VkAllocationCallbacks*, VkShaderModule* module) { + keyedWords = info->codeSize / sizeof(std::uint32_t); + keyedGenerator = info->pCode[2]; + *module = makeHandle(); + return VK_SUCCESS; +} + +void ShaderModuleKeyTests() { + const std::array code{0x07230203u, 0x00010300u, 0x00080001u, 8u, 0u}; + static_cast(AgcDriver::Graphics::CreateShaderModule(keyedCreateShaderModule, VK_NULL_HANDLE, true, code.data(), sizeof(code), "keyed module")); + Require(keyedWords == code.size() && keyedGenerator == 0x00080001u, "a shader module with graphics pipeline libraries changed its generator word"); + static_cast(AgcDriver::Graphics::CreateShaderModule(keyedCreateShaderModule, VK_NULL_HANDLE, false, code.data(), sizeof(code), "keyed module")); + Require(keyedWords == code.size() && keyedGenerator == (0x00080001u ^ 0x8000u), "a shader module without graphics pipeline libraries kept the generator word of the other mode"); + Require(code[2] == 0x00080001u, "keying a shader module changed its source words"); +} + void ProvokingVertexTests() { using namespace AgcDriver::Graphics; constexpr std::uint32_t lastVertex = 1u << 19u; @@ -8652,6 +9363,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"); @@ -9370,6 +10089,24 @@ void highestDrawIndexTests() { Require(HighestDrawIndex(bytesOf(restartOnly), 2, false) == 0xffff, "a 16-bit all-ones index without restart was skipped"); Require(HighestDrawIndex(bytesOf(narrow).first(4), 2, true) == 7 && HighestDrawIndex(bytesOf(narrow).first(2), 2, true) == 0, "the scan read past its index range"); expectFailure([&] { HighestDrawIndex(bytesOf(narrow), 1, false); }, "unsupported index size"); + Require(!HighestDrawIndex({}, 2, false).has_value() && !HighestDrawIndex({}, 4, true).has_value(), "an empty index range reached a vertex"); + std::vector long16(1027, 0xffff); + long16[1026] = 0; + Require(HighestDrawIndex(std::as_bytes(std::span(long16)), 2, true) == 0, "index 0 after many restart indices was not reached"); + long16[513] = 4000; + Require(HighestDrawIndex(std::as_bytes(std::span(long16)), 2, true) == 4000, "a 16-bit index inside a long range was missed"); + std::vector long32(1029); + for (std::size_t i = 0; i < long32.size(); ++i) long32[i] = static_cast(i * 7u % 1000u); + long32[1028] = 0x80000001u; + Require(HighestDrawIndex(std::as_bytes(std::span(long32)), 4, true) == 0x80000001u, "a 32-bit index above 2^31 in the tail was missed"); + long32[1028] = 0xffffffffu; + Require(HighestDrawIndex(std::as_bytes(std::span(long32)), 4, true) == 999u && HighestDrawIndex(std::as_bytes(std::span(long32)), 4, false) == 0xffffffffu, "a 32-bit restart index at the end of a long range"); + std::vector shifted(2 * 1000 + 3); + for (std::size_t i = 0; i < 1000; ++i) { + const auto value = static_cast(i == 777 ? 0x8001u : i % 300u); + std::memcpy(shifted.data() + 1 + 2 * i, &value, sizeof(value)); + } + Require(HighestDrawIndex(std::span(shifted).subspan(1, 2 * 1000 + 1), 2, false) == 0x8001u, "an unaligned 16-bit range with a trailing byte"); } void storeAtFlipTests() { @@ -9652,21 +10389,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); @@ -9674,6 +10426,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}; @@ -9865,13 +10625,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; @@ -9894,7 +10656,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); @@ -9908,9 +10670,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(); @@ -9923,7 +10709,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(); @@ -9966,8 +10752,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; } @@ -10081,6 +10867,11 @@ int main(int argc, char** argv) { std::cout << "Sample location grid fallback checks passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--sample-order") { + sampleOrderTests(); + std::cout << "Sample order checks passed\n"; + return 0; + } { AgcDriver::Graphics::PipelineCacheSynchronization synchronization; auto pipelineUse = synchronization.LockForPipelineUse(); @@ -10141,6 +10932,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{}; @@ -10189,6 +10985,7 @@ int main(int argc, char** argv) { DepthStencilTests(); OneDimensionalColorTests(); htileDepthClearTests(); + multisampledSingleClearTests(); memoryValueFillTests(); ZExportTests(); unorm10_11_11TargetTests(); @@ -10201,6 +10998,7 @@ int main(int argc, char** argv) { stencilReferenceUnusedTests(); aliasedBufferViewTests(); LineStripTests(); + ShaderModuleKeyTests(); ProvokingVertexTests(); ProvokingVertexRenderPassTests(); ConservativeRasterizationTests(); @@ -10231,6 +11029,7 @@ int main(int argc, char** argv) { msaaEnableClearOneSampleTargetTests(); UnwrittenSampleLocationTests(); sampleLocationGridFallbackTests(); + sampleOrderTests(); depthCopyFormatTests(); quadPixelMaskTests(); ShaderStageTests(); @@ -10309,7 +11108,8 @@ int main(int argc, char** argv) { [](std::optional headroom) { mock.memoryLimit = headroom.has_value() ? std::optional(mock.allocatedBytes + *headroom) : std::nullopt; }, - [] { return mock.allocationAttempts; } + [] { return mock.allocationAttempts; }, + [](std::optional limit) { mock.importLimit = limit; } }); Require(mock.live == 0, "BDA resources leaked Vulkan objects"); RunGuestAllocationTests(); 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 d8342211cf..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; @@ -680,4 +685,21 @@ void RunGuestTextureResourceTests() { Fields cornered = base; cornered.cornerSample = true; Require(!SampledTexturesShareEntry(pack(base), pack(cornered)), "T#s differing in a field next to the feedback fields must not share a sampled texture entry"); + + Fields arrayFields = base; + arrayFields.typeRaw = 13; + arrayFields.depth = 1536; + arrayFields.baseArray = 3; + const auto arrayWords = pack(arrayFields); + std::array narrowed{}; + Require(FirstLayerDescriptor(arrayWords, narrowed), "a 2D array T# naming more than its base slice must narrow to that slice"); + const auto slice = DecodeTextureResource(narrowed); + const auto whole = DecodeTextureResource(arrayWords); + Require(slice.dimension == TextureDimension::k2DArray && slice.baseArray == 3 && slice.depthOrLastArray == 3, "a narrowed 2D array T# must name its base slice alone"); + Require(slice.baseAddress == whole.baseAddress && slice.width == whole.width && slice.format == whole.format && slice.mipCount == whole.mipCount && slice.tileMode == whole.tileMode, "narrowing a 2D array T# must keep every other field"); + Require(!SampledTexturesShareEntry(arrayWords, narrowed), "a narrowed 2D array T# must not share the whole array's sampled texture entry"); + Fields single = arrayFields; + single.depth = 3; + Require(!FirstLayerDescriptor(pack(single), narrowed), "a 2D array T# of its base slice alone needs no narrowing"); + Require(!FirstLayerDescriptor(baseWords, narrowed), "a 2D T# needs no narrowing"); } diff --git a/core/libs/prx/libSceAgcDriver/tests/HostHdr.cpp b/core/libs/prx/libSceAgcDriver/tests/HostHdr.cpp index 64b394f21b..6ac8547f1c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/HostHdr.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/HostHdr.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -125,6 +126,29 @@ void hostState() { Expect(!AgcDriver::CurrentHostHdr().available, "a display switched to SDR leaves HDR output available"); } +void forceSdr() { + resetHost(); +#ifdef _WIN32 + _putenv_s("APS5_FORCE_SDR", "1"); +#else + setenv("APS5_FORCE_SDR", "1", 1); +#endif + AgcDriverSetSurfaceHdr10_nid_postfix(true); + AgcDriverSetHostDisplayHdr_nid_postfix(AgcDriver::DisplayHdrFromOutput(true, 800.0f, 0.01f, 400.0f)); + const auto forced = AgcDriver::CurrentHostHdr(); + Expect(!forced.available && forced.display.hdr && forced.surfaceHdr10, "forcing SDR does not suppress HDR availability while preserving host capabilities"); + Expect(AgcDriver::ToneMapPeakNits(forced) == 100.0f, "forcing SDR does not report SDR reference white"); +#ifdef _WIN32 + _putenv_s("APS5_FORCE_SDR", ""); +#else + unsetenv("APS5_FORCE_SDR"); +#endif + AgcDriverSetSurfaceHdr10_nid_postfix(true); + const auto restored = AgcDriver::CurrentHostHdr(); + Expect(restored.available && restored.generation != forced.generation, "removing the SDR override does not restore HDR availability"); + resetHost(); +} + void reportedWhiteLevel() { resetHost(); constexpr std::uint64_t rgbPq = 0x8100070422000000ull; @@ -158,6 +182,7 @@ int main() { whiteLevels(); hostState(); reportedWhiteLevel(); + forceSdr(); if (failures != 0) return 1; std::puts("host HDR tests passed"); return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/HostImportFailure.cpp b/core/libs/prx/libSceAgcDriver/tests/HostImportFailure.cpp new file mode 100644 index 0000000000..939c31b830 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/HostImportFailure.cpp @@ -0,0 +1,176 @@ +#include "HostImportFailure.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#ifdef _WIN32 +#include +#endif +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace AgcDriver::Graphics; + +enum class Step { None, Create, Allocate, Bind }; + +struct Probe { + PFN_vkGetDeviceProcAddr backend = nullptr; + Step fail = Step::None; + VkResult result = VK_SUCCESS; + unsigned injected = 0; + unsigned invalid = 0; + std::set buffers; + std::set memories; +}; + +Probe probe; + +VKAPI_ATTR VkResult VKAPI_CALL CreateBuffer(VkDevice device, const VkBufferCreateInfo* info, const VkAllocationCallbacks* allocator, VkBuffer* buffer) { + if (probe.fail == Step::Create) { + probe.fail = Step::None; + ++probe.injected; + *buffer = reinterpret_cast(std::uintptr_t{0xdeadbeef}); + return probe.result; + } + const auto result = reinterpret_cast(probe.backend(device, "vkCreateBuffer"))(device, info, allocator, buffer); + if (result == VK_SUCCESS) probe.buffers.insert(*buffer); + return result; +} + +VKAPI_ATTR VkResult VKAPI_CALL AllocateMemory(VkDevice device, const VkMemoryAllocateInfo* info, const VkAllocationCallbacks* allocator, VkDeviceMemory* memory) { + if (probe.fail == Step::Allocate) { + probe.fail = Step::None; + ++probe.injected; + *memory = reinterpret_cast(std::uintptr_t{0xdeadbeef}); + return probe.result; + } + const auto result = reinterpret_cast(probe.backend(device, "vkAllocateMemory"))(device, info, allocator, memory); + if (result == VK_SUCCESS) probe.memories.insert(*memory); + return result; +} + +VKAPI_ATTR VkResult VKAPI_CALL BindBufferMemory(VkDevice device, VkBuffer buffer, VkDeviceMemory memory, VkDeviceSize offset) { + if (probe.fail == Step::Bind) { + probe.fail = Step::None; + ++probe.injected; + return probe.result; + } + return reinterpret_cast(probe.backend(device, "vkBindBufferMemory"))(device, buffer, memory, offset); +} + +VKAPI_ATTR void VKAPI_CALL DestroyBuffer(VkDevice device, VkBuffer buffer, const VkAllocationCallbacks* allocator) { + if (probe.buffers.erase(buffer) != 1) { + ++probe.invalid; + return; + } + reinterpret_cast(probe.backend(device, "vkDestroyBuffer"))(device, buffer, allocator); +} + +VKAPI_ATTR void VKAPI_CALL FreeMemory(VkDevice device, VkDeviceMemory memory, const VkAllocationCallbacks* allocator) { + if (probe.memories.erase(memory) != 1) { + ++probe.invalid; + return; + } + reinterpret_cast(probe.backend(device, "vkFreeMemory"))(device, memory, allocator); +} + +VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL DeviceProc(VkDevice device, const char* name) { + if (std::strcmp(name, "vkCreateBuffer") == 0) return reinterpret_cast(CreateBuffer); + if (std::strcmp(name, "vkAllocateMemory") == 0) return reinterpret_cast(AllocateMemory); + if (std::strcmp(name, "vkBindBufferMemory") == 0) return reinterpret_cast(BindBufferMemory); + if (std::strcmp(name, "vkDestroyBuffer") == 0) return reinterpret_cast(DestroyBuffer); + if (std::strcmp(name, "vkFreeMemory") == 0) return reinterpret_cast(FreeMemory); + return probe.backend(device, name); +} + +constexpr std::size_t Alignment = 65536; +constexpr std::size_t Bytes = 4096; + +class Heap { +public: + Heap() { +#ifdef _WIN32 + data = static_cast(VirtualAlloc(nullptr, Alignment, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + data = static_cast(std::aligned_alloc(Alignment, Alignment)); +#endif + Require(data != nullptr, "cannot allocate the host import failure heap"); + std::memset(data, 0x37, Alignment); + GuestAllocations::Mutation().Add(data, Alignment, true, true, true); + } + + ~Heap() { + GuestAllocations::Mutation().Remove(data); +#ifdef _WIN32 + VirtualFree(data, 0, MEM_RELEASE); +#else + std::free(data); +#endif + } + + std::byte* Data() const { return data; } + std::uint64_t Address() const { return reinterpret_cast(data); } + +private: + std::byte* data = nullptr; +}; + +} + +void AgcDriver::Tests::RunHostImportFailureTests(const Graphics::Context& original, bool native) { + unsigned failures = 0; + for (const auto step : {Step::Create, Step::Allocate, Step::Bind}) { + for (const auto result : {VK_ERROR_OUT_OF_HOST_MEMORY, VK_ERROR_OUT_OF_DEVICE_MEMORY}) { + Heap heap; + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + ClearHostImports(original.device); + probe = {}; + probe.backend = original.deviceProc; + probe.fail = step; + probe.result = result; + auto context = original; + context.deviceProc = DeviceProc; + struct Cleanup { + VkDevice device; + ~Cleanup() { ClearHostImports(device); } + } cleanup{context.device}; + SetImportWatch(context, ImportWatch::Unwatch); + const HostImport* failed = nullptr; + bool threw = false; + try { + failed = HostImportFor(context, heap.Address(), Bytes); + } catch (const std::runtime_error&) { + threw = true; + } + const bool rejected = failed == nullptr && probe.injected == 1 && ExistingHostImportFor(context, heap.Address(), Bytes) == nullptr && HostImportLiveBytes() == 0; + const bool released = probe.buffers.empty() && probe.memories.empty(); + const auto invalid = probe.invalid; + ClearHostImports(context.device); + probe.fail = Step::None; + const auto* recovered = HostImportFor(context, heap.Address(), Bytes); + Require(recovered != nullptr && recovered->buffer != VK_NULL_HANDLE && recovered->memory != VK_NULL_HANDLE && recovered->address != 0, "host import retry failed"); + if (native) { + Buffer readback(original, Bytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + std::memset(readback.Bytes().data(), 0, Bytes); + CommandBatch batch(original); + RecordMemoryBarrier(original, batch.Handle(), VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + CopyBuffer(original, batch.Handle(), recovered->buffer, heap.Address() - recovered->base, readback.Handle(), 0, Bytes); + RecordMemoryBarrier(original, batch.Handle(), VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + batch.SubmitAndWait(); + readback.Invalidate(); + Require(std::memcmp(heap.Data(), readback.Bytes().data(), Bytes) == 0, "host import retry returned incorrect GPU readback data"); + } + ClearHostImports(context.device); + const bool clean = probe.buffers.empty() && probe.memories.empty() && HostImportLiveBytes() == 0; + std::printf("host import step=%u result=%d injected=%u rejected=%u threw=%u invalid releases=%u failure cleaned=%u retry cleaned=%u readbacks=%u\n", static_cast(step), static_cast(result), probe.injected, rejected, threw, invalid, released, clean, native ? 1u : 0u); + if (!rejected || !released || !clean || invalid != 0 || probe.invalid != invalid) ++failures; + } + } + Require(failures == 0, "failed host import creation released an undefined handle or leaked a resource"); +} diff --git a/core/libs/prx/libSceAgcDriver/tests/HostImportFailure.hpp b/core/libs/prx/libSceAgcDriver/tests/HostImportFailure.hpp new file mode 100644 index 0000000000..70364786ad --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/HostImportFailure.hpp @@ -0,0 +1,12 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_HOSTIMPORTFAILURE_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_HOSTIMPORTFAILURE_HPP + +#include "prx/libSceAgcDriver/Graphics/include/Context.hpp" + +namespace AgcDriver::Tests { + +void RunHostImportFailureTests(const Graphics::Context& context, bool native); + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/tests/HostImportRetireHold.cpp b/core/libs/prx/libSceAgcDriver/tests/HostImportRetireHold.cpp index 92c6fe639a..8d0d9cddc7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/HostImportRetireHold.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/HostImportRetireHold.cpp @@ -1,3 +1,4 @@ +#include "HostImportFailure.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" @@ -372,6 +373,7 @@ int main() { WindowPromotionOutlivesTheHold(); IdleRetireOutlivesTheHold(); BusyRetireOutlivesALaterBatch(); + AgcDriver::Tests::RunHostImportFailureTests(mockContext(), false); } catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); return 1; diff --git a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp index 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/MeshOutputBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/MeshOutputBudget.cpp new file mode 100644 index 0000000000..01b079f477 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/MeshOutputBudget.cpp @@ -0,0 +1,64 @@ +#include "Recompiler.hpp" +#include +#include +#include +#include + +namespace { + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +ShaderRecompiler::MeshTargetLimits Limits(std::uint32_t vertexGranularity, std::uint32_t primitiveGranularity) { + ShaderRecompiler::MeshTargetLimits limits{}; + limits.maxOutputMemoryBytes = 32768u; + limits.maxOutputComponents = 128u; + limits.outputPerVertexGranularity = vertexGranularity; + limits.outputPerPrimitiveGranularity = primitiveGranularity; + return limits; +} + +ShaderRecompiler::MeshConfiguration Mesh(std::uint32_t vertices, std::uint32_t primitives) { + ShaderRecompiler::MeshConfiguration mesh{}; + mesh.maxVertices = vertices; + mesh.maxPrimitives = primitives; + return mesh; +} + +} + +int main() { + try { + using ShaderRecompiler::MeshBarycentricOutputBudget; + using ShaderRecompiler::MeshOutputUse; + + MeshOutputUse ccleste; + ccleste.flatParameters = 6u; + ccleste.perVertexParameters = 2u; + ccleste.clipVaryings = 1u; + const auto budget = MeshBarycentricOutputBudget(Mesh(128u, 128u), Limits(32u, 32u), ccleste); + Require(budget.vertexLocations == 8u, "per-vertex locations: position, six flat exports and the smooth clip varying make 8"); + Require(budget.primitiveLocations == 10u, "per-primitive locations: three corners, two per-vertex parameters of three vertices and the cull flag make 10"); + Require(budget.components == 72u, "components are 4 per location: 18 locations make 72"); + Require(budget.bytes == 36864u, "bytes: (8 + 10) locations x 16 bytes x 128 = 36864, got " + std::to_string(budget.bytes)); + Require(budget.bytes > Limits(32u, 32u).maxOutputMemoryBytes, "ccleste's configuration does not fit 32768 bytes"); + + Require(MeshBarycentricOutputBudget(Mesh(100u, 100u), Limits(32u, 32u), ccleste).bytes == 36864u, "100 vertices and primitives are padded to the granularity of 32, which gives 128"); + Require(MeshBarycentricOutputBudget(Mesh(128u, 128u), Limits(1u, 1u), ccleste).bytes == 36864u, "a granularity of 1 changes nothing for 128"); + + MeshOutputUse distances = ccleste; + distances.clipDistances = 5u; + distances.cullDistances = 1u; + distances.layer = true; + const auto more = MeshBarycentricOutputBudget(Mesh(128u, 128u), Limits(32u, 32u), distances); + Require(more.vertexLocations == 8u + 2u + 1u, "five clip distances take 2 locations and one cull distance 1"); + Require(more.primitiveLocations == 11u, "the layer output takes one per-primitive location"); + + std::puts("mesh output budget tests passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/PassHazards.cpp b/core/libs/prx/libSceAgcDriver/tests/PassHazards.cpp index 7689e5d90b..25332fe672 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PassHazards.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PassHazards.cpp @@ -34,8 +34,9 @@ struct Use { Images attachments; bool anyReads = false; bool anyWrites = false; + Ranges writeOnly; - PassAccess View() const { return {reads, writes, images, attachments, anyReads, anyWrites}; } + PassAccess View() const { return {reads, writes, images, attachments, anyReads, anyWrites, writeOnly}; } }; void testRangeSet() { @@ -184,12 +185,82 @@ void testAttachments() { } +void testWriteOnly() { + PassHazards hazards; + hazards.BeginPass(); + Use feedback; + feedback.writeOnly = {{0x1000, 0x8f80}}; + hazards.Add(feedback.View()); + Expect(hazards.Check(feedback.View(), true) == PassHazard::None, "draws only storing to the same buffer continue the pass"); + Use reader; + reader.reads = {{0x2000, 0x2100}}; + Expect(hazards.Check(reader.View(), true) == PassHazard::ReadAfterWrite, "a read of write-only stores of the pass"); + Use writer; + writer.writes = {{0x8000, 0x9000}}; + Expect(hazards.Check(writer.View(), true) == PassHazard::WriteAfterWrite, "a read-write use after write-only stores"); + Use unknownReader; + unknownReader.anyReads = true; + Expect(hazards.Check(unknownReader.View(), true) == PassHazard::UnknownRead, "unknown reads after write-only stores"); + Use elsewhere; + elsewhere.writeOnly = {{0x9000, 0xa000}}; + elsewhere.reads = {{0xa000, 0xb000}}; + Expect(hazards.Check(elsewhere.View(), true) == PassHazard::None, "write-only stores and reads elsewhere"); + hazards.Clear(); + hazards.BeginPass(); + Use readWrite; + readWrite.reads = {{0x1000, 0x2000}}; + readWrite.writes = {{0x1000, 0x2000}}; + hazards.Add(readWrite.View()); + Use storesOver; + storesOver.writeOnly = {{0x1800, 0x1900}}; + Expect(hazards.Check(storesOver.View(), true) == PassHazard::WriteAfterWrite, "write-only stores after a read-write use"); + hazards.Clear(); + hazards.BeginPass(); + Use readOnly; + readOnly.reads = {{0x1000, 0x2000}}; + hazards.Add(readOnly.View()); + Expect(hazards.Check(storesOver.View(), true) == PassHazard::WriteAfterRead, "write-only stores after a read"); + hazards.Clear(); + hazards.BeginPass(); + Use unknownWriter; + unknownWriter.anyWrites = true; + unknownWriter.anyReads = true; + hazards.Add(feedback.View()); + Expect(hazards.Check(unknownWriter.View(), true) == PassHazard::UnknownWrite, "unknown writes after write-only stores"); + hazards.Clear(); + hazards.Add(unknownWriter.View()); + Expect(hazards.Check(feedback.View(), true) == PassHazard::UnknownWrite, "write-only stores after unknown writes"); + hazards.Clear(); + Use unknownRead; + unknownRead.anyReads = true; + hazards.Add(unknownRead.View()); + Expect(hazards.Check(feedback.View(), true) == PassHazard::UnknownRead, "write-only stores after unknown reads"); + hazards.Clear(); + hazards.BeginPass(); + hazards.Add(feedback.View()); + hazards.SealWriteOnly(); + Expect(hazards.Check(feedback.View(), true) == PassHazard::WriteAfterWrite, "write-only stores over stores sealed by a flush"); + Use afterSeal; + afterSeal.writeOnly = {{0x9000, 0xa000}}; + Expect(hazards.Check(afterSeal.View(), true) == PassHazard::None, "write-only stores elsewhere after a seal"); + hazards.Add(afterSeal.View()); + Expect(hazards.Check(afterSeal.View(), true) == PassHazard::None, "write-only stores after a seal continue the pass among themselves"); + hazards.Clear(); + Expect(hazards.Check(feedback.View(), true) == PassHazard::None, "a cleared seal"); + hazards.Clear(); + hazards.Add(feedback.View()); + Expect(!hazards.Empty(), "write-only stores are tracked"); + hazards.Clear(); + Expect(hazards.Empty(), "cleared write-only stores"); +} + int main() { testRangeSet(); testRanges(); testUnknown(); testImages(); testAttachments(); + testWriteOnly(); if (failures != 0) { std::fprintf(stderr, "%d pass hazard checks failed\n", failures); return 1; diff --git a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp index 9b5b6531f2..b5db144ac8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp @@ -22,6 +22,12 @@ namespace { +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + using namespace AgcDriver::Graphics; using AgcDriver::OverlayContent; using AgcDriver::OverlayCorner; @@ -353,7 +359,7 @@ void requireTextDrawn(const std::vector& drawn, VkExtent2D extent const auto where = std::string("'") + line[index] + "' of '" + line + "' glyph pixel " + std::to_string(gx) + "," + std::to_string(gy); Require(channel(word, 24) == channel(frameWord, 24), where + " changed the alpha"); if (set) { - Require((word & 0x00ffffffu) == 0x00ffffffu, where + " is not white text: 0x" + std::to_string(word)); + Require((word & 0x00ffffffu) == 0x00ffffffu, where + " is not white text: " + Hex(word)); continue; } for (const unsigned shift : {0u, 8u, 16u}) { 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/Pm4.cpp b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp index 665623b412..fd239f7c00 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp @@ -5,6 +5,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawRegisterKey.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Queues/SubmissionRing.hpp" +#include "prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp" #include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp" #include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" #include "prx/libSceAgcDriver/Submit/include/Acb.hpp" @@ -15,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -1012,6 +1014,54 @@ void testRegisterListsReadAtSubmission() { check(std::atomic_ref(done).load() == 1, "register list rewritten after submission was read by the worker"); } +void testInvalidRegisterListEntries() { + std::array pairs{0x038f038fu, 0xc8e9037eu, 0x10, 41, 0x11, 42}; + const auto packet = makePacket(0x9f, {low(pairs.data()), high(pairs.data()), 0x80000000, 3}); + check(!AgcDriver::Pm4::IndirectRegistersValid(pairs), "an offset dword with bits 16-27 set was taken as a register offset"); + check(!AgcDriver::Pm4::IndirectRegistersValid(std::array{0xffffffffu, 0}), "the sentinel was taken as a register offset"); + check(AgcDriver::Pm4::IndirectRegistersValid(std::array{0x10, 1, 0x70000011u, 2}), "a valid register list was rejected"); + AgcDriver::QueueState state; + expectFailure([&] { AgcDriver::Pm4::ExecuteIndirectRegisters(packet, pairs, state); }, "extended register"); + check(!state.context.contains(0x10) && !state.context.contains(0x11), "a rejected register list partially changed state"); + AgcDriver::Pm4::ExecuteIndirectRegisters(packet, pairs, state, true); + check(state.context.at(0x10) == 41 && state.context.at(0x11) == 42 && !state.context.contains(0x38f), "skipping an invalid entry lost the valid ones or wrote the invalid one"); +} + +void testPartlyUpdatedRegisterListSkipsOnlyItsInvalidEntries() { +#ifdef _WIN32 + check(_putenv_s("APS5_SKIP_INVALID_INDIRECT_REGISTERS", "1") == 0, "cannot enable skipping invalid register list entries"); +#else + check(setenv("APS5_SKIP_INVALID_INDIRECT_REGISTERS", "1", 1) == 0, "cannot enable skipping invalid register list entries"); +#endif + alignas(8) static std::uint32_t gate = 0; + alignas(8) static std::uint32_t copied = 0; + static std::array registers{0x038f038fu, 5, 0x11, 9}; + auto words = joinPackets({ + makePacket(0x3c, {0x13, low(&gate), high(&gate), 1, 0xffffffffu, 0x19}), + makePacket(0x9f, {low(registers.data()), high(registers.data()), 0x80000000, 2}), + makePacket(0x40, {0x100, 0xa010, 0, low(&copied), high(&copied)})}); + submitWords(words); + registers = {0x10, 77, 0x03900390u, 6}; + std::atomic_ref(gate).store(1); + AgcDriverWaitIdle_nid_postfix(); + check(std::atomic_ref(copied).load() == 77, "a register list partly updated before its packet ran lost the current valid entries"); +} + +void testRegisterListReadAgainAtExecution() { + alignas(8) static std::uint32_t gate = 0; + static std::uint32_t done = 0; + static std::array registers{0x038f038fu, 74}; + auto words = joinPackets({ + makePacket(0x3c, {0x13, low(&gate), high(&gate), 1, 0xffffffffu, 0x19}), + makePacket(0x9f, {low(registers.data()), high(registers.data()), 0x80000000, 1}), + writeWord(done, 1)}); + submitWords(words); + registers[0] = 0x10; + std::atomic_ref(gate).store(1); + AgcDriverWaitIdle_nid_postfix(); + check(std::atomic_ref(done).load() == 1, "a register list invalid at submission and valid at execution was not read again"); +} + void testIndexAttributesReadAtExecution() { alignas(8) static std::uint32_t gate = 0; static std::uint32_t done = 0; @@ -1591,6 +1641,53 @@ void testStallWatchdogRewind() { check(std::any_of(lines.begin(), lines.end(), [](const std::string& line) { return line.find("+0x0000 REWIND 00000000") != std::string::npos; }), "the stall dump does not decode the stalled REWIND"); } +void testRewindTailTrace() { + using AgcDriver::DriverDetail::DrawPipeline; + if (DrawPipeline::Depth() == 0) throw std::runtime_error("the REWIND trace test needs the pipelined draw path"); + check(AgcDriver::PerformanceTrace::Configured(), "the REWIND trace test runs without a trace window"); + const char* directory = std::getenv("APS5_TRACE_DIR"); + check(directory != nullptr, "the REWIND trace test has no APS5_TRACE_DIR"); + const auto summaryPath = std::filesystem::path(directory) / "aps5-summary.jsonl"; + std::filesystem::remove(summaryPath); + alignas(8) static std::uint32_t started = 0; + static auto warm = joinPackets({writeWord(started, 1)}); + submitWords(warm); + AgcDriverWaitIdle_nid_postfix(); + check(std::atomic_ref(started).load() == 1, "the queue worker did not start"); + constexpr std::size_t Tails = 12; + alignas(8) static std::array gates{}; + alignas(8) static std::uint32_t done = 0; + static std::vector words; + words.clear(); + for (std::size_t index = 0; index < Tails; ++index) { + const auto rewind = makePacket(0x59, {0x80000000u}); + const auto wait = makePacket(0x3c, {0x13, low(&gates[index]), high(&gates[index]), 1, 0xffffffffu, 0x19}); + words.insert(words.end(), rewind.begin(), rewind.end()); + words.insert(words.end(), wait.begin(), wait.end()); + } + const auto last = writeWord(done, 1); + words.insert(words.end(), last.begin(), last.end()); + AgcDriver::PerformanceTrace::Present(1, 1, nullptr); + submitWords(words); + for (auto& gate : gates) { + std::this_thread::sleep_for(std::chrono::milliseconds(3)); + std::atomic_ref(gate).store(1); + } + AgcDriverWaitIdle_nid_postfix(); + check(std::atomic_ref(done).load() == 1, "the last REWIND tail did not run"); + AgcDriver::PerformanceTrace::Present(1, 1, nullptr); + AgcDriver::PerformanceTrace::StopOutput(); + std::ifstream summary(summaryPath); + std::string line; + check(static_cast(std::getline(summary, line)), "the trace window wrote no summary line"); + const std::string_view key = "\"worker_busy_pct\":{\"queue worker 0x0\":"; + const auto at = line.find(key); + check(at != std::string::npos, "the summary line has no busy share for queue worker 0x0"); + const double busy = std::strtod(line.c_str() + at + key.size(), nullptr); + std::printf("queue worker 0x0 busy over %zu released REWIND tails: %.1f%%\n", Tails, busy); + check(busy <= 150.0, "the queue worker's busy share counts nested REWIND tails more than once"); +} + struct RenderingGateState { std::mutex mutex; std::condition_variable changed; @@ -1818,7 +1915,7 @@ int main(int argc, char** argv) { const std::pair ringModes[]{ {"ring", testSubmissionRing}, {"ring-empty", testEmptySubmissionRing}, {"ring-disabled", testDisabledSubmissionRing}, {"ring-invalid", testInvalidSubmissionRing}, {"ring-report", testSubmissionRingFailureReport}, {"stall-watchdog", testStallWatchdog}, - {"stall-watchdog-invalid", testInvalidStallWatchdog}, {"stall-watchdog-rewind", testStallWatchdogRewind}, {"stall-watchdog-order", testStallWatchdogOrder}}; + {"stall-watchdog-invalid", testInvalidStallWatchdog}, {"stall-watchdog-rewind", testStallWatchdogRewind}, {"stall-watchdog-order", testStallWatchdogOrder}, {"rewind-trace", testRewindTailTrace}}; if (argc == 2 && std::string(argv[1]) == "ring-report-heap") { #ifdef _WIN32 testSubmissionRingFailureReportHeap(); @@ -1864,6 +1961,9 @@ int main(int argc, char** argv) { testUnwrittenUserData(); testDriverSubmission(); testRegisterListsReadAtSubmission(); + testInvalidRegisterListEntries(); + testPartlyUpdatedRegisterListSkipsOnlyItsInvalidEntries(); + testRegisterListReadAgainAtExecution(); testIndexAttributesReadAtExecution(); testPredicatedSubmission(); testConditionalSubmission(); diff --git a/core/libs/prx/libSceAgcDriver/tests/Pm4ReplayTool.cmake b/core/libs/prx/libSceAgcDriver/tests/Pm4ReplayTool.cmake index bc30109c2f..d578167acf 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Pm4ReplayTool.cmake +++ b/core/libs/prx/libSceAgcDriver/tests/Pm4ReplayTool.cmake @@ -27,3 +27,12 @@ execute_process(COMMAND "${TOOL}" "${CAPTURE}/missing" RESULT_VARIABLE code OUTP if(NOT code STREQUAL "2" OR NOT errors MATCHES "cannot open") message(FATAL_ERROR "agc_pm4_replay exited with ${code} instead of 2 on a missing capture:\n${output}${errors}") endif() + +set(unicodeCapture "${CAPTURE}-ação-日本") +file(REMOVE_RECURSE "${unicodeCapture}") +file(COPY "${CAPTURE}/" DESTINATION "${unicodeCapture}") +execute_process(COMMAND "${TOOL}" "${unicodeCapture}" RESULT_VARIABLE code OUTPUT_VARIABLE output ERROR_VARIABLE errors) +file(REMOVE_RECURSE "${unicodeCapture}") +if(NOT code STREQUAL "1" OR NOT output MATCHES "unreleased 1\n") + message(FATAL_ERROR "agc_pm4_replay exited with ${code} on a copy of the capture under a non-ASCII path:\n${output}${errors}") +endif() 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 d8f7bc0bb4..497adb234f 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,10 +59,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -113,6 +117,54 @@ void ReleaseWatched(void* block, std::size_t bytes) { #endif } +bool partialWatchRestorationTests() { +#ifdef _WIN32 + constexpr std::size_t bytes = 131072; + constexpr std::size_t page = 4096; + constexpr std::size_t restoredOffset = 4 * page; + constexpr std::size_t restoredBytes = 4 * page; + if (!AgcDriver::GuestMemory::WriteWatched()) return false; + auto* memory = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, 65536)); + if (memory == nullptr) return false; + GuestArena::GuestArenaCommit_nid_postfix(memory, bytes, PAGE_READWRITE, restoredBytes); + struct Cleanup { + std::uint8_t* memory; + ~Cleanup() { ReleaseWatched(memory, bytes); } + } cleanup{memory}; + const auto address = reinterpret_cast(memory); + AgcDriver::GuestMemory::Unwatch(address, 65536); + Require(!AgcDriver::GuestMemory::Watched(address, page), "the partial remap fixture was not excluded"); + memory[page] = 0x22; + AgcDriver::GuestMemory::MarkWritten(address + page, 1); + Require(AgcDriver::GuestMemory::UntrackedHint(address + page, 1) != 0, "the excluded remap fixture has no driver hint"); + GuestArena::GuestArenaReset_nid_postfix(memory + restoredOffset, restoredBytes); + GuestArena::GuestArenaCommit_nid_postfix(memory + restoredOffset, restoredBytes, PAGE_READWRITE, restoredBytes); + Require(AgcDriver::GuestMemory::Watched(address + restoredOffset, restoredBytes), "a partial private remap did not restore its write watch"); + Require(!AgcDriver::GuestMemory::Watched(address, restoredOffset), "a partial remap restored an excluded prefix"); + Require(!AgcDriver::GuestMemory::Watched(address + restoredOffset + restoredBytes, page), "a partial remap restored an excluded suffix"); + Require(AgcDriver::GuestMemory::UntrackedHint(address + page, 1) == 0, "a partial remap retained a stale driver hint"); + std::memset(memory + restoredOffset, 0x33, restoredBytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + const auto generation = AgcDriver::GuestMemory::CollectWrites(address + restoredOffset, restoredBytes); + Require(generation != 0, "the private remap has no collected CPU write history"); + memory[restoredOffset + page] = 0x44; + AgcDriver::GuestMemory::BumpCollectEpoch(); + Require(AgcDriver::GuestMemory::CollectWrites(address + restoredOffset, restoredBytes) != 0, "the private remap stopped collecting CPU writes"); + Require(!AgcDriver::GuestMemory::UnchangedSince(address + restoredOffset, restoredBytes, generation), "the private remap hid a genuine CPU write"); + GuestArena::GuestArenaReset_nid_postfix(memory, restoredBytes); + GuestArena::GuestArenaCommit_nid_postfix(memory, restoredBytes, PAGE_READWRITE, restoredBytes); + Require(AgcDriver::GuestMemory::Watched(address, restoredBytes), "the first private pages did not regain coverage"); + Require(!AgcDriver::GuestMemory::Watched(address + 12 * page, page), "restoring the first pages restored an excluded tail"); + memory[12 * page] = 0x55; + AgcDriver::GuestMemory::MarkWritten(address + 12 * page, 1); + AgcDriver::GuestMemory::Unwatch(address + 12 * page, page); + Require(AgcDriver::GuestMemory::UntrackedHint(address + 12 * page, 1) == 0, "excluding a partially watched block invented a write-watch hint"); + return true; +#else + return false; +#endif +} + std::optional selectDevice(std::span devices, std::string_view request) { const auto rankDeviceType = [](VkPhysicalDeviceType type) { switch (type) { @@ -1139,6 +1191,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; @@ -2589,6 +2702,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); @@ -2608,7 +2722,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"); } @@ -3546,6 +3660,65 @@ void drawSnapshotTableSetTests(const Device& device, Recorder& recorder) { recorder.Activate(); } +void imageTableFirstLayerRevalidateTests(const Device& device, Recorder& recorder) { + namespace Abi = ShaderRecompiler::ImageTableAbi; + using ShaderRecompiler::DescriptorKind; + using ShaderRecompiler::DescriptorRole; + const auto& base = device.GetContext(); + TextureDetiler detiler(base); + auto context = base; + context.detiler = &detiler; + if (context.imageTableCapacity < 1u) { + std::cout << "image tables unavailable: image table first-layer revalidation not tested\n"; + return; + } + constexpr std::uint32_t width = 4; + constexpr std::uint32_t height = 4; + constexpr std::uint32_t lastSlice = 3; + alignas(256) static std::array texels{}; + texels.fill(1.0f); + const auto address = static_cast(reinterpret_cast(texels.data())); + const std::array texture{static_cast(address >> 8u), static_cast((address >> 40u) & 0xffu) | (22u << 20u) | (((width - 1u) & 3u) << 30u), ((width - 1u) >> 2u) | ((height - 1u) << 14u), 0xfacu | (13u << 28u), lastSlice, 0u, 0u, 0u}; + const auto whole = DecodeTextureResource(texture); + Require(whole.dimension == TextureDimension::k2DArray && whole.depthOrLastArray == lastSlice && DescribeSurface(whole).guestBytes <= sizeof(texels), "the image table test T# is not a 2D array over its texels"); + constexpr auto bindingCount = static_cast(ShaderRecompiler::RuntimeAbi::Binding::Count); + ShaderRecompiler::RecompileResult program; + ShaderRecompiler::DescriptorBinding map{}; + map.kind = DescriptorKind::StorageBuffer; + map.role = DescriptorRole::ImageTableMap; + map.binding = static_cast(ShaderRecompiler::RuntimeAbi::Stage::Main) * bindingCount + static_cast(ShaderRecompiler::RuntimeAbi::Binding::ImageTableMap); + map.count = 1; + map.guestDescriptor.assign(Abi::HeaderWords + 1u, 0u); + map.guestDescriptor[Abi::HeaderVersion] = Abi::Version; + map.guestDescriptor[Abi::HeaderElementCount] = 1u; + map.guestDescriptor[Abi::HeaderElementFaults] = Abi::HeaderWords; + map.guestDescriptor[Abi::HeaderSamplerFaults] = Abi::HeaderWords + 1u; + program.bindings.push_back(map); + ShaderRecompiler::DescriptorBinding images{}; + images.kind = DescriptorKind::SampledImage; + images.role = DescriptorRole::ImageTable; + images.binding = ShaderRecompiler::RuntimeAbi::ImageTableBinding; + images.count = 1; + images.guestDescriptor.assign(texture.begin(), texture.end()); + images.imageShapes.assign(1, ShaderRecompiler::DescriptorImageShape::Image2D); + images.imageDepthCompare.assign(1, false); + images.imageSamplers.assign(1, 0u); + program.bindings.push_back(std::move(images)); + const CompiledShader compute{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + { + ShaderResources resources(context, compute); + Require(resources.SampledTextures().size() == 1, "a 2D read of a 2D array in an image table bound no texture"); + const auto bound = resources.SampledTextures().front(); + ShaderResources::ProofReport report; + Require(resources.Revalidate(std::span(&compute, 1), &report), "an image table holding a 2D read of a 2D array did not revalidate"); + Require(report.path == ShaderResources::ProofPath::Full, "an image table holding a 2D read of a 2D array was not proven on the full path"); + Require(resources.SampledTextures().front() == bound, "revalidating an image table holding a 2D read of a 2D array changed its texture"); + Require(resources.Revalidate(std::span(&compute, 1)), "an image table holding a 2D read of a 2D array did not revalidate twice"); + recorder.Sync(); + } + ClearCachedTextures(context.device); +} + void drawSnapshotReuseTests(const Device& device, Recorder& recorder) { const auto& context = device.GetContext(); constexpr std::size_t bytes = 65536; @@ -5330,7 +5503,7 @@ void splitWriteBackTests(const Device& device, Recorder& recorder) { {"64 KiB R_X 200x150, 3 mips, ranges split at 128 KiB, a second store outside the write watch", TextureTileMode::kR64KBX, 3, 0, 131072, true, true, false, true}, {"linear 200x150, ranges split at 64 KiB, a second store outside the write watch", TextureTileMode::kLinear, 1, 0, 65536, true, true, false, true}, }; - SetHostImportWindow(64ull << 20u); + SetHostImportWindow(64ull << 10u); struct Window { ~Window() { SetHostImportWindow(0); } } window; @@ -5462,6 +5635,113 @@ void splitWriteBackTests(const Device& device, Recorder& recorder) { std::cout << "split write-back: ok\n"; } +void writeBackBlockTests(const Device& device, Recorder& recorder) { + const auto& base = device.GetContext(); + if (base.hostImportAlignment == 0 || 65536 % base.hostImportAlignment != 0) { + std::cout << "host imports unavailable: write-back blocks not tested\n"; + return; + } + constexpr std::size_t piece = 65536; + constexpr std::size_t window = std::size_t{8} << 20u; + struct Case { + const char* name; + std::size_t offset; + std::size_t registeredEnd; + bool alternate; + bool direct; + std::size_t imports; + }; + const Case cases[] = { + {"a 4 MiB image over 64 adjacent 64 KiB pieces", 0, 4u << 20u, false, true, 1}, + {"a 4 MiB image at 64 KiB over 66 adjacent 64 KiB pieces", piece, (4u << 20u) + 2 * piece, false, true, 2}, + {"a 4 MiB image over 64 KiB pieces of which every other one is not GPU mapped", 0, 4u << 20u, true, false, 0}, + }; + SetHostImportWindow(window); + struct Window { + ~Window() { SetHostImportWindow(0); } + } restore; + const auto pattern = [](std::size_t i) { return static_cast(i * 131u + 7u); }; + constexpr std::array red{255, 0, 0, 255}; + std::string failures; + for (const auto& test : cases) { + GuestTextureResource resource{}; + resource.width = 1024; + resource.height = 1024; + resource.mipCount = 1; + resource.tileMode = TextureTileMode::kLinear; + resource.dimension = TextureDimension::k2D; + resource.format = 56; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + const auto surfaceBytes = static_cast(DescribeSurface(resource).guestBytes); + Require(surfaceBytes == (4u << 20u) && test.offset + surfaceBytes <= test.registeredEnd, std::string(test.name) + ": the surface does not fit its pieces"); +#ifdef _WIN32 + void* reserved = VirtualAlloc(nullptr, 2 * window, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); +#else + void* reserved = std::aligned_alloc(window, 2 * window); +#endif + Require(reserved != nullptr, "cannot allocate the write-back block"); + auto* memory = static_cast(reinterpret_cast((reinterpret_cast(reserved) + window - 1) / window * window)); + for (std::size_t i = 0; i < window; ++i) memory[i] = pattern(i); + std::vector pieces; + for (std::size_t offset = 0; offset < test.registeredEnd; offset += piece) pieces.push_back(offset); + { + GuestAllocations::Mutation mutation; + for (const auto offset : pieces) mutation.Add(memory + offset, piece, true, true, !test.alternate || offset / piece % 2 == 0); + } + ClearHostImports(base.device); + const auto address = reinterpret_cast(memory) + test.offset; + const auto before = HostImportCounters(); + TextureDetiler detiler(base); + auto context = base; + context.detiler = &detiler; + resource.baseAddress = address; + std::size_t texels = 0; + bool direct = false; + { + auto image = StorageTexture::Create(context, detiler, resource, 0); + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + const auto commands = recorder.Commands(); + recorder.Keep(image); + const VkClearColorValue value{{1.0f, 0.0f, 0.0f, 1.0f}}; + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + context.Function("vkCmdClearColorImage")(commands, image->Image(), VK_IMAGE_LAYOUT_GENERAL, &value, 1, &range); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + image->MarkDirty(); + image->WriteBack(); + direct = !image->GuestSnapshotValid(); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + for (std::size_t i = test.offset; i < test.offset + surfaceBytes; i += 4) texels += std::equal(red.begin(), red.end(), memory + i) ? 1 : 0; + } + const auto after = HostImportCounters(); + std::size_t made = 0; + std::size_t segments = 0; + for (std::size_t index = 0; index < ImportRequesterCount; ++index) made += after.requesters[index].made - before.requesters[index].made; + segments = after.requesters[static_cast(ImportRequester::Segment)].made - before.requesters[static_cast(ImportRequester::Segment)].made; + std::printf("%s: %zu imports made (%zu by write-back segments), %s write-back, %zu of %zu texels stored\n", test.name, made, segments, direct ? "GPU-direct" : "CPU", texels, surfaceBytes / 4); + if (made != test.imports) failures += std::string(test.name) + ": the write-back made " + std::to_string(made) + " imports, expected " + std::to_string(test.imports) + "\n"; + if (direct != test.direct) failures += std::string(test.name) + ": the write-back took the " + (direct ? "GPU-direct" : "CPU") + " path\n"; + if (texels != surfaceBytes / 4) failures += std::string(test.name) + ": the write-back stored " + std::to_string(texels) + " of " + std::to_string(surfaceBytes / 4) + " texels\n"; + { + GuestAllocations::Mutation mutation; + for (const auto offset : pieces) mutation.Remove(memory + offset); + } + ClearHostImports(base.device); + recorder.Sync(); +#ifdef _WIN32 + VirtualFree(reserved, 0, MEM_RELEASE); +#else + std::free(reserved); +#endif + } + if (!failures.empty()) throw std::runtime_error("write-back blocks:\n" + failures); + std::cout << "write-back blocks: ok\n"; +} + void singlePassTests(const Device& device, Recorder& recorder, bool watched) { const auto& base = device.GetContext(); if (base.hostImportAlignment == 0 || !base.singlePassStorage) { @@ -6398,7 +6678,32 @@ void queuedWriteGroupTests() { } return memory; }; + const auto recorded = [](const std::vector& writes) { + const auto order = Recorder::QueuedWriteOrder(writes); + std::vector ordered; + for (const auto index : order) ordered.push_back(writes[index]); + const auto groups = Recorder::QueuedWriteGroups(ordered); + std::vector sequence; + std::size_t first = 0; + for (const auto end : groups) { + for (const bool computed : {false, true}) { + for (auto i = first; i < end; ++i) { + if (ordered[i].computed == computed) sequence.push_back(order[i]); + } + } + first = end; + } + return std::pair{sequence, groups.size()}; + }; Require(Recorder::QueuedWriteGroups({}).empty(), "no queued writes made a group"); + const std::vector flagged{{0x100, 0x200, false}, {0x100, 0x1f8, true}, {0x108, 0x200, true}, {0x200, 0x208, false}, {0x400, 0x500, false}, {0x400, 0x4f8, true}, {0x408, 0x500, true}, {0x500, 0x508, false}, {0x800, 0x804, false}}; + Require(recorded(flagged) == std::pair{std::vector{0, 4, 1, 2, 5, 6, 3, 7, 8}, std::size_t{2}}, "the dumps of a run with a flag after each query slot were not folded once, with the clears before them and the flags after them"); + const std::vector open{{0x50, 0x58, false}, {0x100, 0x200, false}, {0x100, 0x1f8, true}, {0x108, 0x200, true}, {0x200, 0x208, false}, {0x400, 0x500, false}, {0x400, 0x4f8, true}}; + Require(recorded(open) == std::pair{std::vector{0, 1, 5, 2, 3, 6, 4}, std::size_t{2}}, "the clear of a slot whose end dump the run does not hold was not moved ahead of the dumps"); + const std::vector overDump{{0x100, 0x200, false}, {0x100, 0x1f8, true}, {0x108, 0x200, true}, {0x1f0, 0x410, false}, {0x400, 0x4f8, true}}; + Require(Recorder::QueuedWriteOrder(overDump) == std::vector{0, 1, 2, 3, 4}, "a store over an earlier dump that the later dumps cover only in part was moved ahead of that dump"); + const std::vector overFlag{{0x100, 0x200, false}, {0x100, 0x1f8, true}, {0x108, 0x200, true}, {0x500, 0x508, false}, {0x400, 0x508, false}, {0x400, 0x4f8, true}, {0x408, 0x500, true}}; + Require(Recorder::QueuedWriteOrder(overFlag) == std::vector{0, 1, 2, 3, 4, 5, 6}, "a clear over an earlier store after a dump was moved ahead of that store"); const std::vector query{{0x100, 0x200, false}, {0x100, 0x1f8, true}, {0x108, 0x200, true}, {0x200, 0x300, false}, {0x200, 0x2f8, true}, {0x208, 0x300, true}}; Require(Recorder::QueuedWriteGroups(query) == Groups{3, 6}, "the clear of the next query slot did not wait for the dumps before it"); const std::vector reused{{0x100, 0x1f8, true}, {0x100, 0x200, false}, {0x100, 0x1f8, true}}; @@ -6428,6 +6733,26 @@ void queuedWriteGroupTests() { const auto groups = Recorder::QueuedWriteGroups(writes); Require(!groups.empty() && groups.back() == writes.size() && std::is_sorted(groups.begin(), groups.end()), "queued write groups do not end at the last write in order"); Require(paint(writes, groups, bytes) == programOrder(writes, bytes), "recording each group's stores before its computed writes changed the final bytes of the program order"); + const auto sequence = recorded(writes).first; + std::vector position(writes.size(), writes.size()); + for (std::size_t i = 0; i < sequence.size(); ++i) position[sequence[i]] = i; + Require(sequence.size() == writes.size() && std::find(position.begin(), position.end(), writes.size()) == position.end(), "the recorded order is not a permutation of the queued writes"); + std::vector memory(bytes, 0); + for (const auto index : sequence) { + for (auto at = writes[index].begin; at < writes[index].end; ++at) memory[at] = static_cast(index + 1); + } + Require(memory == programOrder(writes, bytes), "the recorded order changed the final bytes of the program order"); + for (std::size_t j = 0; j < writes.size(); ++j) { + if (writes[j].computed) continue; + std::vector covered(bytes); + for (auto k = j + 1; k < writes.size(); ++k) { + if (writes[k].computed) std::fill(covered.begin() + writes[k].begin, covered.begin() + writes[k].end, true); + } + if (std::any_of(covered.begin() + writes[j].begin, covered.begin() + writes[j].end, [](bool value) { return value; })) continue; + for (std::size_t i = 0; i < j; ++i) { + Require(position[i] < position[j], writes[i].computed ? "a store no later dump overlaps was recorded before a dump ahead of it" : "a store no later dump overlaps was recorded before a store ahead of it"); + } + } } } @@ -7409,10 +7734,11 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) { target.format = VK_FORMAT_R8G8B8A8_UNORM; target.bytes = targetBytes; target.samples = 4; + const bool ordered = MayReorderSamples(context, target.samples) && SampleOrderStorageFormat(context, target.format, target.elementBytes, target.samples) != VK_FORMAT_UNDEFINED; { std::lock_guard gpu(GpuMutex()); Require(MultisampleTargetView(context, target) != VK_NULL_HANDLE, "the multisampled target has no view"); - Require(!MultisampleTargetStorable(context, target), "a multisampled target nothing stored to was created with storage usage"); + Require(MultisampleTargetStorable(context, target) == ordered, ordered ? "a multisampled target whose samples may be reordered was created without storage usage" : "a multisampled target nothing stored to was created with storage usage"); recorder.Submit(); device.WaitQueue(); recorder.Sync(); @@ -7544,9 +7870,9 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) { const CompiledShader storedCompute{ShaderRecompiler::ShaderStage::Compute, &stored, 0}; std::lock_guard gpu(GpuMutex()); const auto rendered = MultisampleTargetView(context, target); - Require(!MultisampleTargetStorable(context, target), "sampling the multisampled target gave it storage usage"); + Require(MultisampleTargetStorable(context, target) == ordered, "sampling the multisampled target changed its storage usage"); ShaderResources resources(context, storedCompute); - Require(MultisampleTargetStorable(context, target) && MultisampleTargetView(context, target) != rendered, "a storage binding did not move the multisampled target to an image with storage usage"); + Require(MultisampleTargetStorable(context, target) && (MultisampleTargetView(context, target) != rendered) != ordered, "a storage binding did not move the multisampled target to an image with storage usage, or moved one that had it"); recorder.Submit(); device.WaitQueue(); recorder.Sync(); @@ -8129,6 +8455,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; @@ -8704,9 +9094,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) { @@ -8789,6 +9184,33 @@ int pipelineCacheFileTests(const Device& device) { PipelineCache reloaded(context, properties); Require(holds(reloaded.Handle(), 1), "an invalid cache file was not replaced by the saving object's pipelines"); } + { + PipelineCache busy(context, properties); + build(busy.Handle(), 0); + std::promise held; + std::promise release; + std::thread creation([&] { + auto use = busy.Synchronization().LockForPipelineUse(); + held.set_value(); + release.get_future().wait(); + }); + held.get_future().wait(); + auto saving = std::async(std::launch::async, [&] { busy.SaveUnlessBusy(); }); + const bool saveReturned = saving.wait_for(std::chrono::seconds(5)) == std::future_status::ready; + bool otherCreationProceeded = false; + if (saveReturned) { + auto other = std::async(std::launch::async, [&] { auto use = busy.Synchronization().LockForPipelineUse(); }); + otherCreationProceeded = other.wait_for(std::chrono::seconds(5)) == std::future_status::ready; + release.set_value(); + other.wait(); + } else { + release.set_value(); + } + creation.join(); + saving.wait(); + Require(saveReturned, "a periodic pipeline cache save waited for a pipeline creation to finish"); + Require(otherCreationProceeded, "a pipeline creation waited behind a periodic pipeline cache save"); + } std::cout << "Pipeline cache file merge tests passed\n"; return 0; } @@ -8951,6 +9373,216 @@ void integerClearTests(const Device& device) { std::cout << "integer storage clears: " << tested << " formats ok\n"; } +bool clearHandOverTests(const Device& device, Recorder& recorder, bool watched) { + const auto& base = device.GetContext(); + const std::string what = watched ? "clear-key hand-over (watched)" : "clear-key hand-over"; + if (base.hostImportAlignment == 0) { + std::cout << "host imports unavailable: " << what << " not tested\n"; + return false; + } + constexpr std::size_t surfaceA = 256 * 256 * 4; + constexpr std::size_t surfaceB = 128 * 128 * 8; + constexpr std::size_t keyCount = surfaceB / 256; + constexpr std::size_t bytes = surfaceA + 65536; + static_assert(surfaceB <= surfaceA && keyCount <= bytes - surfaceA, "B lies over the first units of A, and B's DCC keys follow A inside the block"); + const auto decided = PrepareImportWatch(base); +#ifndef _WIN32 + if (watched && decided == ImportWatch::Unwatch) { + std::cout << "host imports are compared on this device: " << what << " not tested\n"; + return false; + } +#endif + void* block = nullptr; + if (watched) { + block = AllocateWatched(bytes, 65536); + if (block == nullptr) { + std::cout << "no write watching: " << what << " not tested\n"; + return false; + } + } else { +#ifdef _WIN32 + block = VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); +#else + block = std::aligned_alloc(65536, bytes); +#endif + } + Require(block != nullptr && reinterpret_cast(block) % 65536 == 0, "cannot allocate the clear-key hand-over block"); + if (watched) SetImportWatch(base, ImportWatch::Watch); + struct RestoreWatch { + const Context& context; + ImportWatch decided; + ~RestoreWatch() { SetImportWatch(context, decided); } + } restoreWatch{base, decided}; + auto* memory = static_cast(block); + const auto address = reinterpret_cast(block); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true, true); + } + struct Unregister { + const Context& context; + void* block; + std::uint64_t address; + bool watched; + ~Unregister() { + StorageTexture::SetClearHandOver(true); + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + HostImportFor(context, address, bytes); + if (watched) ReleaseWatched(block, bytes); + } + } unregister{base, block, address, watched}; + if (HostImportFor(base, address, bytes) == nullptr) { + std::cout << "host import of the clear-key hand-over block refused: " << what << " not tested\n"; + return false; + } + if (watched && !AgcDriver::GuestMemory::Watched(address, bytes)) { + std::cout << "the imported block left the write watch: " << what << " not tested\n"; + return false; + } + TextureDetiler detiler(base); + auto context = base; + context.detiler = &detiler; + const auto surface = [&](std::uint32_t width, std::uint32_t height, std::uint32_t format, std::uint64_t dcc) { + GuestTextureResource resource{}; + resource.baseAddress = address; + resource.width = width; + resource.height = height; + resource.mipCount = 1; + resource.tileMode = TextureTileMode::kR64KBX; + resource.dimension = TextureDimension::k2D; + resource.format = format; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + resource.dccAddress = dcc; + return resource; + }; + const auto resourceA = surface(256, 256, 56, 0); + const auto resourceB = surface(128, 128, 71, address + surfaceA); + Require(DescribeSurface(resourceA).guestBytes == surfaceA && DescribeSurface(resourceB).guestBytes == surfaceB, "the clear-key hand-over surfaces have unexpected sizes"); + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + const auto draw = [&](const std::shared_ptr& image, VkClearColorValue value) { + const auto commands = recorder.Commands(); + recorder.Keep(image); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + context.Function("vkCmdClearColorImage")(commands, image->Image(), VK_IMAGE_LAYOUT_GENERAL, &value, 1, &range); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + image->MarkDirty(); + }; + const auto readImage = [&](const std::shared_ptr& image, std::uint32_t width, std::uint32_t height, std::size_t texelBytes) { + Buffer readback(context, static_cast(width) * height * texelBytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + const auto commands = recorder.Commands(); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + VkBufferImageCopy copy{}; + copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + copy.imageExtent = {width, height, 1}; + context.Function("vkCmdCopyImageToBuffer")(commands, image->Image(), VK_IMAGE_LAYOUT_GENERAL, readback.Handle(), 1, ©); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + const auto pixels = readback.Bytes(); + return std::vector(pixels.begin(), pixels.end()); + }; + const auto readMemory = [&](std::uint64_t from, std::size_t length) { + std::vector read(length); + AgcDriver::GuestMemory::Read(from, read); + recorder.Sync(); + if (!watched) std::memcpy(read.data(), memory + (from - address), length); + return read; + }; + const auto filled = [](std::span read, std::span texel) { + for (std::size_t i = 0; i < read.size(); ++i) { + if (std::to_integer(read[i]) != texel[i % texel.size()]) return false; + } + return true; + }; + constexpr std::array red{255, 0, 0, 255}; + constexpr std::array blue{0, 0, 255, 255}; + constexpr std::array green{0, 0, 0x00, 0x3c, 0, 0, 0x00, 0x3c}; + constexpr std::array cleared{}; + enum class Order { Superseded, ReadFirst, Rewritten, CpuStore }; + struct Outcome { + std::vector bytes; + std::vector imageB; + std::uint64_t handOvers = 0; + std::uint64_t superseded = 0; + }; + const auto run = [&](bool on, Order order) { + const std::string where = what + (order == Order::Superseded ? ", A never read again" : order == Order::ReadFirst ? ", A read before B wrote" : order == Order::Rewritten ? ", A written again after the hand-over" : ", a CPU store before B wrote") + (on ? ", on" : ", off"); + StorageTexture::SetClearHandOver(on); + recorder.Sync(); + std::memset(memory, 0x55, surfaceA); + std::memset(memory + surfaceA, 0x00, keyCount); + Outcome outcome{}; + const auto handOvers = StorageTexture::ClearHandOvers(); + const auto superseded = StorageTexture::HandedOverSuperseded(); + { + auto imageA = StorageTexture::Create(context, detiler, resourceA, 0); + auto imageB = StorageTexture::Create(context, detiler, resourceB, 0); + draw(imageA, {{1.0f, 0.0f, 0.0f, 1.0f}}); + imageB->Refresh(); + if (on && watched) { + Require(StorageTexture::ClearHandOvers() == handOvers + 1, where + ": B's refresh under clear keys did not hand A's results over"); + Require(StorageTexture::FindPending(address, surfaceA) == imageA, where + ": A's results did not stay pending"); + Require(std::all_of(memory, memory + 2 * 65536, [](std::uint8_t value) { return value == 0x55; }), where + ": B's refresh stored A's results over B"); + } else { + Require(StorageTexture::ClearHandOvers() == handOvers, where + ": the hand-over ran while off"); + } + outcome.imageB = readImage(imageB, 128, 128, 8); + Require(filled(outcome.imageB, cleared), where + ": B does not hold its keys' clear"); + if (order == Order::ReadFirst) { + Require(filled(readMemory(address, 65536), red), where + ": a read of A's memory before B wrote it did not see A's results"); + imageA->Refresh(); + Require(filled(readImage(imageA, 256, 256, 4), red), where + ": A read again as A lost its results"); + } + if (order == Order::Rewritten) { + draw(imageA, {{0.0f, 0.0f, 1.0f, 1.0f}}); + Require(filled(readMemory(address, 65536), blue), where + ": a read of A's memory did not see A's newer results"); + imageB->Refresh(); + Require(filled(readImage(imageB, 128, 128, 8), cleared), where + ": B refreshed again does not hold its keys' clear"); + } + if (order == Order::CpuStore) { + std::memset(memory + 65536, 0x77, 65536); + } + draw(imageB, {{0.0f, 1.0f, 0.0f, 1.0f}}); + Require(StorageTexture::FlushPending(address, bytes, nullptr, "clear-key hand-over test", PublishScope::Whole), where + ": nothing was pending at the end"); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + outcome.bytes = readMemory(address, surfaceA); + Require(filled(readImage(imageB, 128, 128, 8), green), where + ": B lost its results"); + } + outcome.handOvers = StorageTexture::ClearHandOvers() - handOvers; + outcome.superseded = StorageTexture::HandedOverSuperseded() - superseded; + recorder.Sync(); + return outcome; + }; + for (const auto order : {Order::Superseded, Order::ReadFirst, Order::Rewritten, Order::CpuStore}) { + const std::string name = what + (order == Order::Superseded ? ", A never read again" : order == Order::ReadFirst ? ", A read before B wrote" : order == Order::Rewritten ? ", A written again after the hand-over" : ", a CPU store before B wrote"); + const auto off = run(false, order); + const auto on = run(true, order); + const auto& lastA = order == Order::Rewritten ? blue : red; + if (order != Order::CpuStore) Require(filled(std::span(off.bytes).first(2 * 65536), green), name + ": B's results did not reach guest memory with the hand-over off"); + Require(filled(std::span(off.bytes).subspan(2 * 65536), lastA), name + ": A's results did not reach guest memory with the hand-over off"); + Require(on.imageB == off.imageB, name + ": B held other texels with the hand-over on than off"); + Require(on.bytes == off.bytes, name + ": the hand-over left other guest bytes than the old order"); + Require(off.handOvers == 0 && off.superseded == 0, name + ": the hand-over ran while off"); + if (!watched) { + Require(on.handOvers == 0, name + ": results were handed over in memory the write tracker does not watch"); + continue; + } + if (order == Order::Superseded) Require(on.superseded == 2, name + ": B's results did not drop A's two units under B (dropped " + std::to_string(on.superseded) + ")"); + if (order == Order::ReadFirst) Require(on.superseded <= 1, name + ": a unit stored by the read was dropped again"); + } + std::cout << what << ": ok\n"; + return true; +} + void denormalClearTests(const Device& device, Recorder& recorder) { const auto& base = device.GetContext(); if (base.hostImportAlignment == 0) { @@ -9313,6 +9945,11 @@ void Run(Context context) { int main(int argc, char** argv) { try { + if (argc == 2 && std::string_view(argv[1]) == "--partial-watch-restoration-only") { + if (!partialWatchRestorationTests()) return 77; + std::cout << "partial private write-watch restoration tests passed\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--device-selection-only") { deviceSelectionTests(); std::cout << "recorder device selection tests passed\n"; @@ -9447,6 +10084,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; } @@ -9462,6 +10100,14 @@ int main(int argc, char** argv) { keptUnitSettleTests(device, recorder); return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--clear-hand-over-only") { + clearHandOverTests(device, recorder, false); + if (!clearHandOverTests(device, recorder, true)) { + std::cout << "skipped, the device imports no watched host memory for the clear-key hand-over\n"; + return 77; + } + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--integer-clear-only") { integerClearTests(device); return 0; @@ -9495,6 +10141,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); @@ -9550,6 +10201,14 @@ int main(int argc, char** argv) { std::cout << "CMASK copied clear, write-back and failure tests passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--writeback-blocks-only") { + if (device.GetContext().hostImportAlignment == 0) { + std::cout << "skipped, host imports are unavailable\n"; + return 77; + } + writeBackBlockTests(device, recorder); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--metadata-pass-only") { metadataPassTests(device, recorder); std::cout << "Color metadata pass tests passed\n"; @@ -9606,6 +10265,7 @@ int main(int argc, char** argv) { if (argc == 2 && std::string_view(argv[1]) == "--draw-descriptors-only") { drawDescriptorWriteTests(device, recorder); drawSnapshotTableSetTests(device, recorder); + imageTableFirstLayerRevalidateTests(device, recorder); std::cout << "Draw descriptor writes and snapshot overrides passed\n"; return 0; } @@ -9665,6 +10325,7 @@ int main(int argc, char** argv) { drawSnapshotReuseTests(device, recorder); drawDescriptorWriteTests(device, recorder); drawSnapshotTableSetTests(device, recorder); + imageTableFirstLayerRevalidateTests(device, recorder); drawSnapshotCapTests(device, recorder); drawSnapshotEvictionTests(device); unwrittenSnapshotTests(device); @@ -9682,6 +10343,8 @@ int main(int argc, char** argv) { movedMetadataTests(device, recorder); viewPastLastMipTests(device, recorder); keysFillTests(device, recorder); + clearHandOverTests(device, recorder, false); + clearHandOverTests(device, recorder, true); integerClearTests(device); denormalClearTests(device, recorder); unitShadowTests(device, recorder); @@ -9691,6 +10354,7 @@ int main(int argc, char** argv) { writeBackPaddingTests(device, recorder, false); writeBackPaddingTests(device, recorder, true); splitWriteBackTests(device, recorder); + writeBackBlockTests(device, recorder); singlePassTests(device, recorder, false); singlePassTests(device, recorder, true); unchangedCpuStampTests(device, recorder); @@ -9706,6 +10370,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 f335fb2c69..8ef8dfde37 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -41,6 +41,9 @@ #define WIN32_LEAN_AND_MEAN #endif #include +#include +#include +#include "prx/libc/include/General.hpp" #endif namespace { @@ -59,6 +62,40 @@ void setEnvironment(const char* name, const std::string& value) { #endif } +#ifdef _WIN32 +void verifyHostStandardError() { + const auto directory = std::filesystem::temp_directory_path() / ("aps5-shader-host-stderr-" + std::to_string(std::random_device{}())); + setEnvironment("ANYPS5_NO_SHADER_CACHE", "0"); + setEnvironment("ANYPS5_SHADER_CACHE_DIR", directory.string()); + int capture[2], guest[2]; + require(::_pipe(capture, 65536, _O_BINARY) == 0, "cannot create host capture"); + require(::_pipe(guest, 65536, _O_BINARY) == 0, "cannot create guest capture"); + const int original = ::_dup(2); + require(original >= 0 && ::_dup2(capture[1], 2) == 0, "cannot capture initial stderr"); + const int saved = ::_dup(2); + require(saved >= 0 && AdoptHostStandardStream_nid_no_patch(2, saved), "cannot preserve host stderr"); + require(::_dup2(guest[1], 2) == 0, "cannot redirect guest stderr"); + require(ShaderDiskCache::Enabled(), "shader disk cache was disabled"); + ShaderDiskCache::Shutdown(); + std::fflush(stderr); + std::fflush(HostStandardErrorFile_nid_no_patch()); + require(::_dup2(original, 2) == 0, "cannot restore stderr"); + ::_close(original); + DWORD hostBytes = 0, guestBytes = 0; + require(::PeekNamedPipe(reinterpret_cast(::_get_osfhandle(capture[0])), nullptr, 0, nullptr, &hostBytes, nullptr) != 0, "cannot inspect host capture"); + require(::PeekNamedPipe(reinterpret_cast(::_get_osfhandle(guest[0])), nullptr, 0, nullptr, &guestBytes, nullptr) != 0, "cannot inspect guest capture"); + std::string host(hostBytes, '\0'); + if (hostBytes != 0) require(::_read(capture[0], host.data(), hostBytes) == static_cast(hostBytes), "cannot read host capture"); + ::_close(capture[0]); + ::_close(capture[1]); + ::_close(guest[0]); + ::_close(guest[1]); + std::filesystem::remove_all(directory); + require(guestBytes == 0, "shader cache diagnostic entered the guest stderr pipe"); + require(host.find("[shader-disk-cache]") != std::string::npos, "shader cache diagnostic did not reach host stderr"); +} +#endif + void removeCacheDirectory(const std::filesystem::path& directory) { std::error_code error; #ifdef _WIN32 @@ -132,6 +169,7 @@ void requireSameResult(const RecompileResult& left, const RecompileResult& right require(left.shaderDataUserWords == right.shaderDataUserWords, prefix + "shader-data user words differ"); require(left.imageTableRanges == right.imageTableRanges && left.imageTableShader == right.imageTableShader && left.imageTableFaults == right.imageTableFaults && left.imageTablePoison.size() == right.imageTablePoison.size(), prefix + "image table invocation data differs"); require(left.imageTableReadRanges == right.imageTableReadRanges, prefix + "image table read ranges differ"); + require(left.userDataRegisterReads == right.userDataRegisterReads, prefix + "user data register read mask differs"); for (std::size_t i = 0; i < left.imageTablePoison.size(); ++i) { const auto& a = left.imageTablePoison[i]; const auto& b = right.imageTablePoison[i]; @@ -144,6 +182,7 @@ void requireSameResult(const RecompileResult& left, const RecompileResult& right require(a.location == b.location && a.components == b.components && a.resource.fields == b.resource.fields && a.fetchIndex == b.fetchIndex && a.formatComponents == b.formatComponents, prefix + "vertex attribute differs"); } require(left.barycentricEmulation.active == right.barycentricEmulation.active && left.barycentricEmulation.smooth == right.barycentricEmulation.smooth && left.barycentricEmulation.linear == right.barycentricEmulation.linear, prefix + "barycentric emulation differs"); + require(left.meshBarycentrics == right.meshBarycentrics, prefix + "mesh barycentric layout differs"); } void requireSameVariant(const CompiledVariant& left, const CompiledVariant& right, const char* what) { @@ -224,12 +263,14 @@ RecompileResult sampleResult() { result.poisonedSrtReads = 3; result.shaderDataUserWords = {2u, 0xffffffffu, 9u}; result.barycentricEmulation = {true, false, true}; + result.meshBarycentrics = MeshBarycentricLayout{{true, true, false}, {FragmentParameter{0, 2, false, true}, FragmentParameter{3, 3, false, false}}}; result.variantId = 99; result.imageTablePoison = {{{1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u}, 8u, 2u, 6u}, {{9u, 0u, 0u, 0u, 0u, 0u, 0u, 0u}, 4u, 0xffffffffu, 19u}}; result.imageTableRanges = {{0x1000u, 0x200u}, {0x5000000000ull, 48u}}; result.imageTableReadRanges = {{{0x1000u, 32u}, {0x1040u, 32u}}, {}, {{0x5000000000ull, 16u}}}; result.imageTableShader = 0x80003aa700ull; result.imageTableFaults = 3u; + result.userDataRegisterReads = (std::uint64_t{1} << 63u) | (std::uint64_t{1} << 32u) | 0x25u; return result; } @@ -278,9 +319,11 @@ CompiledVariant sampleVariant() { image.cubeFaceCompatible = false; image.flatVolumeCompatible = false; image.singleMipPlaneBiasCompatible = true; + image.volumeArraySampleCompatible = true; image.flatLineCompatible = false; image.flatVolumeLineCompatible = false; image.foldTexelOffsetsOnly = true; + image.remappedDepth1D = true; image.byElements = 4; image.byComponents = 1; image.emulatedCompare = 0x1a07u; @@ -593,6 +636,43 @@ void verifyPrebuiltAcrossProcesses(const char* self, const std::filesystem::path require(std::system(command.c_str()) == 0, "the prebuilt loading process failed"); } +void verifyPrebuiltDiscovery(std::string_view layout, bool seed) { + using namespace ShaderRecompiler::ShaderDiskCache; + require(layout == "pack" || layout == "loose" || layout == "versioned" || layout == "file" || layout == "empty" || layout == "missing", "unknown prebuilt discovery layout"); + Shutdown(); + SampleRequest sample = prebuiltSample(); + const auto key = sample.Key(); + const auto variant = sampleVariant(); + if (seed) { + const auto directory = ShaderRecompiler::ShaderCacheDirectory() / "prebuilt"; + const auto entry = EncodeEntry(key, variant); + if (layout == "pack") { + auto otherKey = key; + otherKey.back() ^= std::byte{1}; + require(WriteFileAtomically(directory / "a.pack", packBytes({{key, entry}})), "cannot seed the matching prebuilt pack"); + require(WriteFileAtomically(directory / "z.pack", packBytes({{otherKey, EncodeEntry(otherKey, variant)}})), "cannot seed the other prebuilt pack"); + } else if (layout == "loose") { + require(WriteFileAtomically(directory / EntryName(key), entry), "cannot seed the loose prebuilt entry"); + } else if (layout == "versioned") { + require(WriteFileAtomically(directory / EntryDirectory().filename() / EntryName(key), entry), "cannot seed the versioned prebuilt entry"); + } else if (layout == "file") { + require(WriteFileAtomically(directory, packBytes({{key, entry}})), "cannot seed the direct prebuilt pack"); + } else if (layout == "empty") { + require(WriteFileAtomically(directory / "ignored.txt", entry), "cannot seed the ignored prebuilt file"); + } + std::cout << "prebuilt discovery seeded " << layout << '\n'; + return; + } + CompiledVariant loaded; + const bool expected = layout != "empty" && layout != "missing"; + require(Load(key, loaded) == expected, "prebuilt discovery returned the wrong result for " + std::string(layout)); + if (expected) requireSameVariant(variant, loaded, "prebuilt discovery"); + const auto totals = Totals(); + require(totals.prebuiltHits == (expected ? 1u : 0u) && totals.hits == 0 && totals.writes == 0 && Writers() == 0, + "prebuilt discovery did not remain a read-only prebuilt lookup"); + std::cout << "prebuilt discovery verified " << layout << '\n'; +} + void verifyArtifactStorageIsolation() { SampleRequest sample; const auto key = sample.Key(); @@ -610,6 +690,18 @@ void verifyArtifactStorageIsolation() { require(ShaderDiskCache::EncodeEntry(key, variant) != before, "the stored artifact ignores vertex input metadata"); } +void verifyMeshBarycentricRequestSerialization() { + for (const auto& layout : {std::optional{}, std::optional{MeshBarycentricLayout{{true, true, true}, {FragmentParameter{0, 2, false, true}, FragmentParameter{4, 4, false, false}}}}}) { + SampleRequest sample; + sample.request.shader.stage = ShaderStage::Mesh; + sample.request.context.compute.reset(); + sample.request.graphics = GraphicsCompileContext{0, {}, MeshConfiguration{4u, 4u, 12u, 12u, 4u, 64u, 1024u, 0u, 4u}, std::nullopt, {}, layout}; + const auto text = RequestSerializer{}.Serialize(sample.request); + const auto restored = RequestSerializer{}.Deserialize(text); + require(restored.request.graphics.has_value() && restored.request.graphics->meshBarycentrics == layout, "a captured mesh request lost its barycentric layout"); + } +} + void verifyKeySensitivity() { SampleRequest base; const auto key = base.Key(); @@ -652,8 +744,10 @@ void verifyKeySensitivity() { changes("the narrow subgroup clock", [](SampleRequest& sample) { sample.request.target.narrowSubgroupClock = true; }); changes("f64 denormal preservation", [](SampleRequest& sample) { sample.request.target.float64DenormPreserve = true; }); changes("f16 denormal preservation", [](SampleRequest& sample) { sample.request.target.float16DenormPreserve = true; }); + changes("f32 denormal preservation", [](SampleRequest& sample) { sample.request.target.float32DenormPreserve = true; }); changes("spanned waves", [](SampleRequest& sample) { sample.request.target.spannedWaves = true; }); changes("robust buffer access 2", [](SampleRequest& sample) { sample.request.target.robustBufferAccess2 = true; }); + changes("wave LDS memory barriers", [](SampleRequest& sample) { sample.request.target.waveLdsMemoryBarriers = true; }); changes("the workgroup size limit", [](SampleRequest& sample) { sample.request.target.maxWorkgroupSize[2] = 128; }); changes("the invocation limit", [](SampleRequest& sample) { sample.request.target.maxWorkgroupInvocations = 512; }); changes("the shared memory limit", [](SampleRequest& sample) { sample.request.target.maxWorkgroupSharedMemoryBytes = 32768; }); @@ -678,6 +772,40 @@ void verifyKeySensitivity() { require(changed.first != astro.first && changed.second != astro.second, std::string("the key ignores ") + what); } + const auto meshKeys = [&](const std::optional& layout) { + SampleRequest sample; + sample.request.shader.stage = ShaderStage::Mesh; + sample.request.context.compute.reset(); + sample.request.graphics = GraphicsCompileContext{0, {}, MeshConfiguration{4u, 4u, 12u, 12u, 4u, 64u, 1024u, 0u, 4u}, std::nullopt, {}, layout}; + return std::pair{sample.Key(), RecompileCacheKey::ContextHash(sample.request)}; + }; + const std::vector>> meshLayouts{ + {"no mesh barycentric layout", std::nullopt}, + {"smooth mesh barycentrics", MeshBarycentricLayout{{true, true, false}, {}}}, + {"linear mesh barycentrics", MeshBarycentricLayout{{true, false, true}, {}}}, + {"smooth and linear mesh barycentrics", MeshBarycentricLayout{{true, true, true}, {}}}, + {"a per-vertex mesh parameter", MeshBarycentricLayout{{true, true, false}, {FragmentParameter{0, 0, false, true}}}}, + {"another per-vertex mesh parameter source", MeshBarycentricLayout{{true, true, false}, {FragmentParameter{0, 1, false, true}}}}, + {"a flat mesh parameter", MeshBarycentricLayout{{true, true, false}, {FragmentParameter{0, 0, false, false}}}}, + }; + for (std::size_t i = 0; i < meshLayouts.size(); ++i) { + require(meshKeys(meshLayouts[i].second) == meshKeys(meshLayouts[i].second), std::string("the key of ") + meshLayouts[i].first + " is not deterministic"); + for (std::size_t j = i + 1; j < meshLayouts.size(); ++j) { + const auto left = meshKeys(meshLayouts[i].second); + const auto right = meshKeys(meshLayouts[j].second); + require(left.first != right.first && left.second != right.second, std::string("the mesh key confuses ") + meshLayouts[i].first + " with " + meshLayouts[j].first); + } + } + + const auto pixelKeys = [&](bool meshPath) { + SampleRequest sample; + sample.request.shader.stage = ShaderStage::Fragment; + sample.request.context.compute.reset(); + sample.request.graphics = GraphicsCompileContext{0, {}, meshPath ? std::optional(MeshConfiguration{4u, 4u, 12u, 12u, 4u, 64u, 1024u, 0u, 4u}) : std::nullopt, std::nullopt, {}, std::nullopt}; + return std::pair{sample.Key(), RecompileCacheKey::ContextHash(sample.request)}; + }; + require(pixelKeys(true).first != pixelKeys(false).first && pixelKeys(true).second != pixelKeys(false).second, "a pixel shader on the mesh path shares a key with the vertex path"); + SampleRequest moved; moved.userData[0] ^= 0x10000u; moved.request.shader.codeAddress += 0x1000000u; @@ -744,6 +872,13 @@ int runLargeFileWrite() { std::error_code error; std::filesystem::remove_all(directory, error); std::filesystem::create_directories(directory, error); + struct Cleanup { + std::filesystem::path directory; + ~Cleanup() { + std::error_code error; + std::filesystem::remove_all(directory, error); + } + } cleanup{directory}; const auto space = std::filesystem::space(directory, error); if (error || space.available < size + (std::uintmax_t{1} << 30u)) { std::cout << "skipping: not enough free disk space for a file of more than 2 GiB\n"; @@ -770,6 +905,8 @@ int runLargeFileWrite() { }).detach(); if (result.wait_for(std::chrono::seconds(200)) != std::future_status::ready) { std::cerr << "WriteFileAtomically did not return within 200 s for a file of more than 2 GiB\n"; + std::filesystem::remove_all(directory, error); + if (error) std::cerr << "cannot remove the large file test directory: " << error.message() << '\n'; std::cerr.flush(); std::_Exit(1); } @@ -924,6 +1061,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}; @@ -1672,7 +1865,7 @@ IrResourcePlan handBuiltPlan() { wide.SetImmediateU64(0x8000000000000001ull); material.dwords[3] = &wide; plan.descriptorSources = {buffer, table, heap, material}; - plan.controlFlow = {{&condition, {1u, 2u}, {0u}, {}}, {nullptr, {2u}, {3u}, {0u}}, {nullptr, {}, {}, {}}}; + plan.controlFlow = {{&condition, {1u, 2u}, {0u}, {}, {1u}}, {nullptr, {2u}, {3u}, {0u}, {0u, 1u}}, {nullptr, {}, {}, {}, {}}}; plan.materializationSources = {0u, 2u}; plan.srtReads = {{&readConst(4), 2u}, {&pointer, 1u}}; plan.guardedSrtSlots = {1u}; @@ -1754,7 +1947,7 @@ bool samePlan(const IrResourcePlan& left, const IrResourcePlan& right) { for (std::size_t index = 0; index < left.controlFlow.size(); ++index) { const auto& a = left.controlFlow[index]; const auto& b = right.controlFlow[index]; - if (!sameValue(a.condition, b.condition) || a.successors != b.successors || a.sources != b.sources || a.images != b.images) return false; + if (!sameValue(a.condition, b.condition) || a.successors != b.successors || a.sources != b.sources || a.images != b.images || a.flatSlots != b.flatSlots) return false; } for (std::size_t index = 0; index < left.srtReads.size(); ++index) { if (!sameValue(left.srtReads[index].value, right.srtReads[index].value) || left.srtReads[index].flatOffset != right.srtReads[index].flatOffset) return false; @@ -2259,6 +2452,14 @@ void verifyDefaultDirectory(const char* self) { int main(int argc, char** argv) { try { + if (argc == 2 && std::string_view(argv[1]) == "--housekeeping-safety") { + const auto directory = ShaderDiskCache::EntryDirectory(); + require(!directory.empty(), "the housekeeping fixture cache is disabled"); + ShaderDiskCache::Shutdown(); + require(ShaderDiskCache::Writers() == 0, "the housekeeping writer did not stop"); + std::cout << "housekeeping version " << directory.filename().string() << '\n'; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--resource-plan-codec") { verifyResultRoundTrip(); verifyEntryRoundTrip(); @@ -2272,6 +2473,20 @@ 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]) == "--host-stderr") { + verifyHostStandardError(); + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--long-host-paths") { + verifyLongHostPaths(); + return 0; + } +#endif + if (argc == 3 && (std::string_view(argv[1]) == "--seed-prebuilt-discovery" || std::string_view(argv[1]) == "--verify-prebuilt-discovery")) { + verifyPrebuiltDiscovery(argv[2], std::string_view(argv[1]) == "--seed-prebuilt-discovery"); + return 0; + } 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(); @@ -2288,6 +2503,7 @@ int main(int argc, char** argv) { verifyPrebuiltPack(directory / "packs"); verifyArtifactStorageIsolation(); verifyKeySensitivity(); + verifyMeshBarycentricRequestSerialization(); verifyFloatModeSerialization(); verifyPlanRoundTrip(); verifyPlanKey(); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 55658b2fee..a754f42271 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -15,6 +15,7 @@ #include "Optimization/SrtWalker/SrtInstructionPredicates.hpp" #include "Optimization/SrtWalker/SrtFlatSlotClasses.hpp" #include "Optimization/SrtWalker/SrtTape.hpp" +#include "RdnaDecoder/RdnaInstructionDecoder.hpp" #include "Optimization/SrtWalker.hpp" #include "IntermediateRepresentation/IrBuilder.hpp" #include "IntermediateRepresentation/IrProgram.hpp" @@ -603,6 +604,10 @@ void verifyMayEvaluate() { auto& firstLane = make(IrOpcode::ReadFirstLane, IrType::U32, {&lane, &varying}); auto& firstActive = make(IrOpcode::ReadFirstLane, IrType::U32, {&make(IrOpcode::SelectU32, IrType::U32, {&varying, &input, &lane}), &varying}); auto& firstInactive = make(IrOpcode::ReadFirstLane, IrType::U32, {&make(IrOpcode::SelectU32, IrType::U32, {&varying, &lane, &input}), &varying}); + auto& conjMask = make(IrOpcode::LogicalAnd, IrType::Bool, {&varying, &uniform}); + auto& firstActiveConjunct = make(IrOpcode::ReadFirstLane, IrType::U32, {&make(IrOpcode::SelectU32, IrType::U32, {&varying, &input, &lane}), &conjMask}); + auto& otherCond = make(IrOpcode::IEqual32, IrType::Bool, {&lane, &constant(5)}); + auto& firstNotImplied = make(IrOpcode::ReadFirstLane, IrType::U32, {&make(IrOpcode::SelectU32, IrType::U32, {&otherCond, &input, &lane}), &conjMask}); plan.srtReads = {{&input, 0u}, {&lane, 4u}, {nullptr, 8u}}; auto& constInput = make(IrOpcode::ReadConst, IrType::U32, {&constant(0), &constant(0)}); auto& constLane = make(IrOpcode::ReadConst, IrType::U32, {&constant(0), &constant(1)}); @@ -617,7 +622,7 @@ void verifyMayEvaluate() { plan.srtReads[3].value = &make(IrOpcode::ReadFirstLane, IrType::U32, {&constFirstItself, &varying}); std::array data{4}; SrtRuntime runtime{data}; - for (const auto& [value, expected, name] : std::array, 21>{{ + for (const auto& [value, expected, name] : std::array, 23>{{ {&constant(3), true, "a constant"}, {&input, true, "user data"}, {&uniform, true, "a comparison of user data"}, @@ -633,6 +638,8 @@ void verifyMayEvaluate() { {&firstLane, false, "a first-lane read of a vector register"}, {&firstActive, true, "a first-lane read of a select whose active arm is user data"}, {&firstInactive, false, "a first-lane read of a select whose active arm is a vector register"}, + {&firstActiveConjunct, true, "a first-lane read of a select whose condition is implied by a conjunct mask"}, + {&firstNotImplied, false, "a first-lane read of a select whose condition is not implied by the mask"}, {&constInput, true, "a constant read whose slot holds user data"}, {&constLane, false, "a constant read whose slot holds a vector register"}, {&constOutside, false, "a constant read past the last slot"}, @@ -645,6 +652,16 @@ void verifyMayEvaluate() { std::uint32_t result = 0; require(evaluator.Evaluate(Detail::IrNode(value), result) == expected, (std::string("MayEvaluate disagreed with the evaluator on ") + name).c_str()); } + require(SrtWalker().ValidateRuntimeValue(plan, &firstActiveConjunct), "ValidateRuntimeValue rejects select on a conjunct of active mask"); + require(!SrtWalker().ValidateRuntimeValue(plan, &firstNotImplied), "ValidateRuntimeValue accepts select not implied by active mask"); + auto& countBits = make(IrOpcode::BitCount32, IrType::U32, {&input}); + auto& findILsb = make(IrOpcode::FindILsb32, IrType::U32, {&input}); + auto& findUMsb = make(IrOpcode::FindUMsb32, IrType::U32, {&input}); + Detail::Evaluator bitEvaluator(plan, runtime); + std::uint32_t bitResult = 0; + require(bitEvaluator.Evaluate(Detail::IrNode(&countBits), bitResult) && bitResult == 1u, "Evaluator evaluated BitCount32 incorrectly"); + require(bitEvaluator.Evaluate(Detail::IrNode(&findILsb), bitResult) && bitResult == 2u, "Evaluator evaluated FindILsb32 incorrectly"); + require(bitEvaluator.Evaluate(Detail::IrNode(&findUMsb), bitResult) && bitResult == 2u, "Evaluator evaluated FindUMsb32 incorrectly"); const std::array takenAsEvaluable{IrOpcode::Identity, IrOpcode::Phi, IrOpcode::Void, IrOpcode::GetUserData, IrOpcode::GetShaderBase, IrOpcode::LoadAddressU32, IrOpcode::ReadConstBuffer}; for (std::uint16_t raw = 0; raw < static_cast(IrOpcode::Count); ++raw) { const auto opcode = static_cast(raw); @@ -834,7 +851,8 @@ void verifyOutOfRangeConstantReads() { require(plan->info.images.size() == 2u && plan->info.samplers.size() == 1u, "out-of-range s_buffer_load: the two T#s were not planned as one image each"); AgcDriver::ShaderMemory memory({}); const auto capture = memory.Capture(request); - request.context.memory = memory.Regions(); + const auto memoryRegions = memory.Regions(); + request.context.memory = memoryRegions; static_cast(Recompile(request, *capture)); const auto holds = [&](const std::array& words) { return std::ranges::any_of(capture->snapshot.images, [&](const DescriptorValue& value) { @@ -1009,8 +1027,10 @@ void verifyGuardedDescriptorSources() { fill(valid, mode); AgcDriver::ShaderMemory memory({}); const auto capture = memory.Capture(storeRequest); - storeRequest.context.memory = memory.Regions(); - return Recompile(storeRequest, *capture); + const auto memoryRegions = memory.Regions(); + auto compileRequest = storeRequest; + compileRequest.context.memory = memoryRegions; + return Recompile(compileRequest, *capture); }; const auto storePlan = GetResourcePlan(storeRequest); require(storePlan->info.images.size() == 2u && storePlan->descriptorSources.size() == 1u, "guarded images: the two stores were not planned as two images of one T#"); @@ -3447,6 +3467,90 @@ void verifyPointerChainImageTable() { store(links[1].data() + 4u, address(descriptors[1].data())); } +void verifyWaterfallImageTable() { + using namespace ShaderRecompiler; + namespace Abi = ImageTableAbi; + static TableTexture textures[2]; + alignas(256) static std::array root{}; + static std::array keys{}; + static std::array output{}; + const auto keysV = TableV(keys.data(), 0u, static_cast(sizeof(keys))); + const auto outputV = TableV(output.data(), 0u, static_cast(sizeof(output))); + std::copy(keysV.begin(), keysV.end(), root.begin()); + std::copy(outputV.begin(), outputV.end(), root.begin() + 4); + const auto at = [&](std::uint32_t offset, std::array words) { + std::copy(words.begin(), words.end(), root.begin() + offset / 4u); + }; + at(0x40u + 2u * 32u, TableT(textures[0].bytes.data(), TableFormat8888UNorm, TableType2D)); + at(0x40u + 4u * 32u, TableT(textures[1].bytes.data(), TableFormat32UInt, TableType2D)); + const auto base = static_cast(reinterpret_cast(root.data())); + const std::array userData{static_cast(base), static_cast(base >> 32u)}; + const std::vector code{0xf4080700u, 0xfa000010u, 0xf4080800u, 0xfa000020u, 0x7e220500u, 0x8f128511u, 0xf40c0900u, 0x24000040u, + 0xf09c0f08u, 0x01090000u, 0xe0700000u, 0x80070000u, 0xbf810000u}; + auto request = TableRequest(code, userData); + request.shader.codeAddress = 0x38000u; + const auto plan = GetResourcePlan(request); + require(plan->info.images.size() == 1u && plan->info.images[0].table == 0u, "waterfall image table: the T# was not planned as a table"); + const auto& column = *plan->descriptorSources.at(plan->info.images[0].source).tableColumn; + require(column.address && column.stride == 32u && column.offset == 0x40u && column.dwordCount == 8u && column.unkeyed, "waterfall image table: the column is wrong"); + const auto compiled = CompileTable(request); + const auto& snapshot = compiled.capture->snapshot.tables.tables.at(0); + require(snapshot.keys == Abi::MaxKeys && (snapshot.codes[2] & Abi::PoisonFlag) == 0u && (snapshot.codes[4] & Abi::PoisonFlag) == 0u, "waterfall image table: the snapshot failed"); + auto boundedCode = code; + boundedCode.insert(boundedCode.begin() + 4, {0x7d8a0080u, 0x7e020284u, 0x02000282u}); + auto boundedRequest = TableRequest(boundedCode, userData); + boundedRequest.shader.codeAddress = 0x39000u; + const auto boundedPlan = GetResourcePlan(boundedRequest); + require(boundedPlan->info.images.size() == 1u && boundedPlan->info.images[0].table == 0u, "waterfall image table: bounded key was not planned as a table"); + const auto& boundedColumn = *boundedPlan->descriptorSources.at(boundedPlan->info.images[0].source).tableColumn; + require(boundedColumn.maxKey == 6u, "waterfall image table: bounded key maxKey is wrong"); + const auto boundedCompiled = CompileTable(boundedRequest); + const auto& boundedSnapshot = boundedCompiled.capture->snapshot.tables.tables.at(0); + require(boundedSnapshot.keys == 7u && boundedSnapshot.keys < Abi::MaxKeys && (boundedSnapshot.codes[2] & Abi::PoisonFlag) == 0u && (boundedSnapshot.codes[4] & Abi::PoisonFlag) == 0u, "waterfall image table: bounded key snapshot failed"); + IrResourcePlan planA; + std::vector> values; + const auto make = [&](IrOpcode opcode, IrType type, std::initializer_list arguments = {}) -> IrValue& { + values.push_back(std::make_unique(opcode, type, static_cast(values.size()))); + for (auto* argument : arguments) values.back()->AddArgument(argument); + return *values.back(); + }; + const auto constant = [&](std::uint32_t val) -> IrValue& { + auto& leaf = make(IrOpcode::Void, IrType::U32); + leaf.SetImmediateU32(val); + return leaf; + }; + auto& scalar = make(IrOpcode::Void, IrType::ScalarReg); + scalar.SetRegister({RegisterBank::Scalar, 0}); + auto& vector = make(IrOpcode::Void, IrType::VectorReg); + vector.SetRegister({RegisterBank::Vector, 0}); + auto& input = make(IrOpcode::GetUserData, IrType::U32, {&scalar}); + auto& lane = make(IrOpcode::GetRegister, IrType::U32, {&vector}); + auto& condA = make(IrOpcode::IEqual32, IrType::Bool, {&input, &constant(4)}); + auto& condB = make(IrOpcode::IEqual32, IrType::Bool, {&input, &lane}); + auto& conjMask = make(IrOpcode::LogicalAnd, IrType::Bool, {&condB, &condA}); + auto& selectConj = make(IrOpcode::SelectU32, IrType::U32, {&condB, &input, &lane}); + auto& firstConj = make(IrOpcode::ReadFirstLane, IrType::U32, {&selectConj, &conjMask}); + require(SrtWalker().ValidateRuntimeValue(planA, &firstConj), "waterfall image table: conjunct select was refused by validator"); + planA.srtPlanComplete = true; + planA.descriptorSources.push_back({}); + planA.descriptorSources.back().dwordCount = 1; + planA.descriptorSources.back().dwords[0] = &firstConj; + planA.materializationSources.push_back(0); + const std::array userDataA{42u}; + SrtRuntime runtimeA{userDataA}; + const auto* tapeA = Detail::SrtTape::For(planA); + require(tapeA != nullptr, "waterfall image table: tape was not built for conjunct select"); + std::vector resultsA; + std::vector flatA; + std::vector activeA; + require(tapeA->Run(planA, runtimeA, resultsA, flatA, activeA), "waterfall image table: tape failed to run conjunct select"); + require(!resultsA.empty() && resultsA[0].dwords[0] == 42u, "waterfall image table: tape evaluated conjunct select to wrong value"); + auto& otherCond = make(IrOpcode::IEqual32, IrType::Bool, {&lane, &constant(5)}); + auto& selectOther = make(IrOpcode::SelectU32, IrType::U32, {&otherCond, &input, &lane}); + auto& firstOther = make(IrOpcode::ReadFirstLane, IrType::U32, {&selectOther, &conjMask}); + require(!SrtWalker().ValidateRuntimeValue(planA, &firstOther), "waterfall image table: non-implied condition was accepted by validator"); +} + void verifyDescriptorPhis() { using namespace ShaderRecompiler; constexpr std::uint32_t Format8888UNorm = 56; @@ -3514,7 +3618,8 @@ void verifyDescriptorPhis() { require(plan->info.images.size() == images && plan->info.samplers.size() == samplers && plan->info.sampledPairs.size() == 2u, "descriptor Phi: the edges were not given one resource each"); AgcDriver::ShaderMemory memory({}); const auto capture = memory.Capture(request); - request.context.memory = memory.Regions(); + const auto memoryRegions = memory.Regions(); + request.context.memory = memoryRegions; const auto compiled = Recompile(request, *capture); require(countOps(compiled->spirv, OpImageSampleExplicitLod) == 2u, "descriptor Phi: the specialized SPIR-V does not sample once per edge"); #if ANYPS5_ENABLE_SPIRV_TOOLS @@ -3545,7 +3650,8 @@ void verifyDescriptorPhis() { auto twoLane = makeRequest(samplerCode, 64u); AgcDriver::ShaderMemory twoLaneMemory({}); const auto twoLaneCapture = twoLaneMemory.Capture(twoLane); - twoLane.context.memory = twoLaneMemory.Regions(); + const auto twoLaneRegions = twoLaneMemory.Regions(); + twoLane.context.memory = twoLaneRegions; require(countOps(Recompile(twoLane, *twoLaneCapture)->spirv, OpImageSampleExplicitLod) == 4u, "descriptor Phi: the specialized two-lane SPIR-V does not sample once per edge and half"); auto dynamic = makeRequest(dynamicCode); @@ -3567,7 +3673,8 @@ void verifyDescriptorPhis() { require(sequentialPlan->info.images.size() == 2u && sequentialPlan->info.samplers.size() == 2u, "descriptor Phi: a T# and an S# chosen in separate blocks were not split into their SRT descriptors"); AgcDriver::ShaderMemory sequentialMemory({}); const auto sequentialCapture = sequentialMemory.Capture(sequential); - sequential.context.memory = sequentialMemory.Regions(); + const auto sequentialRegions = sequentialMemory.Regions(); + sequential.context.memory = sequentialRegions; static_cast(Recompile(sequential, *sequentialCapture)); const auto planRestoredSampler = [&](std::span code, const char* message) { @@ -3576,7 +3683,8 @@ void verifyDescriptorPhis() { require(plan->info.samplers.size() == 1u && plan->info.sampledPairs.size() == 1u, message); AgcDriver::ShaderMemory memory({}); const auto capture = memory.Capture(request); - request.context.memory = memory.Regions(); + const auto memoryRegions = memory.Regions(); + request.context.memory = memoryRegions; static_cast(Recompile(request, *capture)); }; std::vector restoredSamplerCode{0xf40c0200u, 0xfa000000u, 0xf4080500u, 0xfa000020u, 0xf4080600u, 0xfa000040u, 0xbe930380u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20400u, 0xbe900380u, 0xbe910380u, 0xbe9c0414u, 0xbe9e0416u, 0xbf068011u, 0xbf850001u, 0x80128112u, 0xbe94041cu, 0xbe96041eu, 0x80118111u, 0xbf0a8211u, 0xbf85fff6u, 0xf09c0f08u, 0x00a20000u, 0x80108110u, 0xbf0a8210u, 0xbf85fff0u, 0x80138113u, 0xbf0a8213u, 0xbf85ffecu, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xe0700000u, 0x80060400u, 0x7d880090u, 0xbea23c6au, 0x4a020081u, 0xe0700000u, 0x80060100u, 0xbf810000u}; @@ -3750,7 +3858,8 @@ void verifyLaneSelectedDescriptors() { require(plan->info.images.size() == 2u && plan->info.samplers.size() == 1u, "lane-selected T#: the T#s selected per lane were not planned as one image each"); AgcDriver::ShaderMemory memory({}); const auto capture = memory.Capture(request); - request.context.memory = memory.Regions(); + const auto memoryRegions = memory.Regions(); + request.context.memory = memoryRegions; static_cast(Recompile(request, *capture)); const auto holds = [&](const std::array& words) { return std::ranges::any_of(capture->snapshot.images, [&](const DescriptorValue& value) { @@ -3796,7 +3905,8 @@ void verifyProgramCounterRelativeData() { }; AgcDriver::ShaderMemory memory({}); static_cast(memory.Capture(request)); - request.context.memory = memory.Regions(); + const auto memoryRegions = memory.Regions(); + request.context.memory = memoryRegions; const auto first = Recompile(request); require(dataBase(first) == codeAddress + 56u, "program counter data: the V# does not name the data at the shader's address"); auto relocated = request; @@ -4739,7 +4849,7 @@ void verifyPixelRequestSerialization() { minimal.context.pixel->layerExported = true; minimal.context.pixel->ditheredAlphaToCoverage = true; const auto encoded = serializer.Serialize(minimal); - require(requestPrefix(encoded, 8u) == "NVNQQSQAAAA=", "new requests did not use serialization version 36"); + require(requestPrefix(encoded, 8u) == "NVNQQSoAAAA=", "new requests did not use serialization version 42"); const std::array fallbacks{0x40u, 0x1ac4u}; auto withFallbacks = minimal; withFallbacks.context.linkedCallFallbacks = fallbacks; @@ -4857,7 +4967,7 @@ void verifyPixelRequestSerialization() { for (std::size_t bytes = 0; bytes < 45u; ++bytes) { expectFailure([&] { static_cast(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source, alpha-to-coverage, stencil reference export flag, primitive class, layer export, dither or front-face flag, quad pixel mask or quad sample mask was accepted"); } - for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQSUAAAA="}) { + for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQSsAAAA="}) { expectFailure([&] { static_cast(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted"); } static constexpr std::string_view version18Request{ @@ -4950,6 +5060,7 @@ void verifyVertexInfoVersion19() { auto version20Vertex = flagged; version20Vertex.pop_back(); version20Vertex.pop_back(); + version20Vertex.pop_back(); version20Vertex[4] = 20u; const auto replay20Vertex = serializer.Deserialize(encodeBase64(version20Vertex)); require(replay20Vertex.request.context.vertex.has_value() && replay20Vertex.request.context.vertex->paClVsOutCntl == 0x89abcdefu, "a main version20 request lost PA_CL_VS_OUT_CNTL"); @@ -4960,6 +5071,7 @@ void verifyVertexInfoVersion19() { auto legacy = plain; legacy.pop_back(); legacy.pop_back(); + legacy.pop_back(); legacy.erase(legacy.begin() + offset, legacy.begin() + offset + 4u); for (const auto version : {16u, 17u, 18u, 19u}) { legacy[4] = static_cast(version); @@ -4996,7 +5108,40 @@ void verifyVertexInfoVersion19() { require(allBitsOffset == flagOffset + 2u, "the front-face all-bits flag does not follow the dithered coverage flag"); require(blendOffset == allBitsOffset + 18u, "the reversed-order blend code is not serialized after the front-face all-bits flag, quad pixel mask and quad sample masks"); constexpr std::size_t sampleMaskBytes = 16u; - constexpr std::size_t iterationBytes = 34u; + constexpr std::size_t iterationBytes = 35u; + constexpr std::size_t sampleOrderBytes = 16u; + const auto orderOffset = blendOffset + 4u + iterationBytes; + auto ordered = pixel; + ordered.context.pixel->sampleOrder = {0x76543210u, 0x76543210u, 0x76543201u, 0x76543201u}; + const auto orderedBytes = decodeBase64(serializer.Serialize(ordered)); + require(orderedBytes.size() == plainPixel.size() && std::equal(plainPixel.begin(), plainPixel.begin() + static_cast(orderOffset), orderedBytes.begin()) && orderedBytes[orderOffset] == 0x10u && orderedBytes[orderOffset + 11u] == 0x76u && orderedBytes[orderOffset + 12u] == 0x01u && std::equal(plainPixel.begin() + static_cast(orderOffset + sampleOrderBytes), plainPixel.end(), orderedBytes.begin() + static_cast(orderOffset + sampleOrderBytes)), "the sample order is not the four words the version 42 pixel info adds after the stencil operation value export flag"); + auto orderedVersion34 = orderedBytes; + orderedVersion34.pop_back(); + orderedVersion34.pop_back(); + orderedVersion34.pop_back(); + orderedVersion34.erase(orderedVersion34.begin() + static_cast(orderOffset - 1u), orderedVersion34.begin() + static_cast(orderOffset + sampleOrderBytes)); + orderedVersion34[4] = 34u; + const auto orderedReplay34 = serializer.Deserialize(encodeBase64(orderedVersion34)); + require(orderedReplay34.request.context.pixel.has_value() && orderedReplay34.request.context.pixel->sampleOrder == std::array{} && serializer.Serialize(orderedReplay34.request) == encodedPixel, "a version 34 request read a sample order, or misread the fields after its pixel iteration fields"); + auto orderedVersion30 = orderedBytes; + orderedVersion30.resize(orderedVersion30.size() - 4u); + orderedVersion30.erase(orderedVersion30.begin() + static_cast(blendOffset + 4u), orderedVersion30.begin() + static_cast(orderOffset + sampleOrderBytes)); + orderedVersion30[4] = 30u; + const auto orderedReplay30 = serializer.Deserialize(encodeBase64(orderedVersion30)); + require(orderedReplay30.request.context.pixel.has_value() && orderedReplay30.request.context.pixel->sampleOrder == std::array{} && serializer.Serialize(orderedReplay30.request) == encodedPixel, "a version 30 request read a sample order, or misread the fields after its reversed-order blend code"); + auto orderedSpanned = ordered; + orderedSpanned.target.spannedWaves = true; + auto orderedVersion32 = decodeBase64(serializer.Serialize(orderedSpanned)); + orderedVersion32.pop_back(); + require(orderedVersion32.size() > 2u && orderedVersion32.back() == 0u && orderedVersion32[orderedVersion32.size() - 2u] == 0u && orderedVersion32[orderedVersion32.size() - 3u] == 1u, "the spanned-waves flag is not the byte before the depth remap flag"); + orderedVersion32.pop_back(); + orderedVersion32.pop_back(); + orderedVersion32.erase(orderedVersion32.begin() + static_cast(blendOffset + 4u), orderedVersion32.begin() + static_cast(orderOffset + sampleOrderBytes)); + orderedVersion32[4] = 32u; + const auto orderedReplay32 = serializer.Deserialize(encodeBase64(orderedVersion32)); + auto unordered = pixel; + unordered.target.spannedWaves = true; + require(orderedReplay32.request.context.pixel.has_value() && orderedReplay32.request.context.pixel->sampleOrder == std::array{} && orderedReplay32.request.target.spannedWaves && serializer.Serialize(orderedReplay32.request) == serializer.Serialize(unordered), "a version 32 request read a sample order or lost its spanned-waves flag"); auto maskedPixel = pixel; maskedPixel.context.pixel->quadPixelMask = 0x2u; const auto maskedBytes = decodeBase64(serializer.Serialize(maskedPixel)); @@ -5011,17 +5156,19 @@ void verifyVertexInfoVersion19() { version30.pop_back(); version30.pop_back(); version30.pop_back(); - version30.erase(version30.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u), version30.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes)); + version30.pop_back(); + version30.erase(version30.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u), version30.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); version30[4] = 30u; const auto replay30 = serializer.Deserialize(encodeBase64(version30)); require(replay30.request.context.pixel.has_value() && !replay30.request.context.pixel->iteratesSamples && replay30.request.context.pixel->positionLocation == PixelPositionLocation::Center && replay30.request.context.pixel->sampleLocations == std::array{} && decodeBase64(serializer.Serialize(replay30.request)) == sampledBytes, "a version 30 request read pixel iteration, a position location or sample locations, or misread the fields after its reversed-order blend code"); auto spanned = sampledPixel; spanned.target.spannedWaves = true; auto version32 = decodeBase64(serializer.Serialize(spanned)); + version32.pop_back(); require(version32.size() > 2u && version32.back() == 0u && version32[version32.size() - 2u] == 0u && version32[version32.size() - 3u] == 1u, "the spanned-waves flag is not the byte before the depth remap flag"); version32.pop_back(); version32.pop_back(); - version32.erase(version32.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u), version32.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes)); + version32.erase(version32.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u), version32.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); version32[4] = 32u; const auto replay32 = serializer.Deserialize(encodeBase64(version32)); require(replay32.request.context.pixel.has_value() && !replay32.request.context.pixel->iteratesSamples && replay32.request.context.pixel->sampleLocations == std::array{} && replay32.request.target.spannedWaves && serializer.Serialize(replay32.request) == serializer.Serialize(spanned), "a version 32 request read pixel iteration or sample locations, or lost its spanned-waves flag"); @@ -5029,7 +5176,14 @@ void verifyVertexInfoVersion19() { version29.pop_back(); version29.pop_back(); version29.pop_back(); - version29.erase(version29.begin() + static_cast(maskOffset + 1u + sampleMaskBytes), version29.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes)); + version29.pop_back(); + version29.erase(version29.begin() + static_cast(maskOffset + 1u + sampleMaskBytes), version29.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); + auto version36 = sampledBytes; + version36.pop_back(); + version36.erase(version36.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes - 1u), version36.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); + version36[4] = 36u; + const auto replay36 = serializer.Deserialize(encodeBase64(version36)); + require(replay36.request.context.pixel.has_value() && !replay36.request.context.pixel->stencilOperationValueExport && decodeBase64(serializer.Serialize(replay36.request)) == sampledBytes, "a version 36 request read a stencil operation value export or misread the fields after the sample locations"); version29[4] = 29u; const auto replay29 = serializer.Deserialize(encodeBase64(version29)); require(replay29.request.context.pixel.has_value() && replay29.request.context.pixel->quadSampleMask == std::array{0x1u, 0x2u, 0x4u, 0x8u} && replay29.request.context.pixel->reversedBlend == 0u && decodeBase64(serializer.Serialize(replay29.request)) == sampledBytes, "a version 29 request read a reversed-order blend code or misread the fields after the quad sample masks"); @@ -5037,7 +5191,8 @@ void verifyVertexInfoVersion19() { version28.pop_back(); version28.pop_back(); version28.pop_back(); - version28.erase(version28.begin() + static_cast(maskOffset + 1u), version28.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes)); + version28.pop_back(); + version28.erase(version28.begin() + static_cast(maskOffset + 1u), version28.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); version28[4] = 28u; const auto replay28 = serializer.Deserialize(encodeBase64(version28)); require(replay28.request.context.pixel.has_value() && replay28.request.context.pixel->quadPixelMask == 0x2u && replay28.request.context.pixel->quadSampleMask == std::array{0xffffu, 0xffffu, 0xffffu, 0xffffu} && replay28.request.context.pixel->reversedBlend == 0u && decodeBase64(serializer.Serialize(replay28.request)) == maskedBytes, "a version 28 request read quad sample masks or a reversed-order blend code, or misread the fields after them"); @@ -5045,7 +5200,8 @@ void verifyVertexInfoVersion19() { version27.pop_back(); version27.pop_back(); version27.pop_back(); - version27.erase(version27.begin() + static_cast(maskOffset), version27.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes)); + version27.pop_back(); + version27.erase(version27.begin() + static_cast(maskOffset), version27.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); version27[4] = 27u; const auto replay27 = serializer.Deserialize(encodeBase64(version27)); require(replay27.request.context.pixel.has_value() && replay27.request.context.pixel->quadPixelMask == 0xfu && replay27.request.context.pixel->reversedBlend == 0u && serializer.Serialize(replay27.request) == encodedPixel, "a version 27 request read a quad pixel mask or reversed-order blend code, or misread the fields after it"); @@ -5053,7 +5209,8 @@ void verifyVertexInfoVersion19() { legacyPixel.pop_back(); legacyPixel.pop_back(); legacyPixel.pop_back(); - legacyPixel.erase(legacyPixel.begin() + static_cast(allBitsOffset), legacyPixel.begin() + static_cast(allBitsOffset + 2u + sampleMaskBytes + 4u + iterationBytes)); + legacyPixel.pop_back(); + legacyPixel.erase(legacyPixel.begin() + static_cast(allBitsOffset), legacyPixel.begin() + static_cast(allBitsOffset + 2u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); legacyPixel[4] = 26u; const auto replay26 = serializer.Deserialize(encodeBase64(legacyPixel)); require(replay26.request.context.pixel.has_value() && replay26.request.context.pixel->rasterPrimitive == RasterPrimitive::Rectangle && !replay26.request.context.pixel->frontFaceAllBits && replay26.request.context.pixel->reversedBlend == 0u, "a version 26 request lost its pixel primitive class or read a front-face all-bits flag"); @@ -5102,9 +5259,10 @@ void verifyDepthRemapRequests() { require(serializer.Serialize(replay.request) == remapped, "a version 35 depth remap request changed on re-serialization"); const auto plainBytes = decodeBase64(plain); const auto remappedBytes = decodeBase64(remapped); - require(plainBytes.size() == remappedBytes.size() && plainBytes.size() > 5u && std::equal(plainBytes.begin(), plainBytes.end() - 6, remappedBytes.begin()), "the depth remap flag and mode are not the last 6 bytes before the f32 flush flag of a vertex request"); + require(plainBytes.size() == remappedBytes.size() && plainBytes.size() > 5u && std::equal(plainBytes.begin(), plainBytes.end() - 7, remappedBytes.begin()), "the depth remap flag and mode are not the 6 bytes before the f32 flush and denormal preserve flags of a vertex request"); for (const std::uint8_t legacyVersion : {32u, 34u}) { auto legacyBytes = remappedBytes; + legacyBytes.pop_back(); legacyBytes[4] = legacyVersion; legacyBytes.resize(legacyBytes.size() - 6u); const auto legacy = serializer.Deserialize(encodeBase64(legacyBytes)); @@ -5112,6 +5270,7 @@ void verifyDepthRemapRequests() { require(serializer.Serialize(legacy.request) == plain, ("a version " + std::to_string(legacyVersion) + " vertex request misread the fields before the depth remap").c_str()); } auto version35 = remappedBytes; + version35.pop_back(); version35[4] = 35u; version35.pop_back(); const auto replay35 = serializer.Deserialize(encodeBase64(version35)); @@ -5138,6 +5297,44 @@ void verifyDepthRemapRequests() { require(key != other, "the depth remap mode did not change the shader cache key"); } +void verifyMergedHullConfiguration() { + using namespace ShaderRecompiler; + static constexpr std::array code{0xbf810000u}; + const RequestSerializer serializer; + RecompileRequest request{}; + request.shader = {ShaderStage::TessellationControl, 0x10000u, code, 0, {}}; + request.context.waveSize = 64; + request.context.vertex = ShaderVertexStageInfo{}; + request.graphics = GraphicsCompileContext{0u, {}, std::nullopt, TessellationConfiguration{3u, 3u, 1u, 2u, 2u}, {}}; + const auto separate = serializer.Serialize(request); + std::vector separateKey; + RecompileCacheKey::Build(request, separateKey); + request.graphics->tessellation->mergedHull = true; + const auto merged = serializer.Serialize(request); + std::vector mergedKey; + RecompileCacheKey::Build(request, mergedKey); + const auto replay = serializer.Deserialize(merged); + require(replay.request.graphics.has_value() && replay.request.graphics->tessellation.has_value() && replay.request.graphics->tessellation->mergedHull, "a merged hull configuration was lost in serialization"); + require(!serializer.Deserialize(separate).request.graphics->tessellation->mergedHull, "a separate hull configuration was read as merged"); + auto legacy = decodeBase64(merged); + legacy.pop_back(); + require(legacy.size() > 52u && legacy[legacy.size() - 52u] == 1u, "the merged hull flag is not the last byte of the tessellation configuration"); + legacy.erase(legacy.end() - 52); + for (const auto version : {30u, 31u, 32u, 34u, 35u, 36u, 37u, 38u}) { + auto older = legacy; + older[4] = static_cast(version); + if (version < 38u) older.erase(older.end() - 31); + if (version < 36u) older.pop_back(); + if (version < 35u) older.resize(older.size() - 5u); + if (version < 32u) older.pop_back(); + const auto upgraded = serializer.Deserialize(encodeBase64(older)); + const auto what = "a main version " + std::to_string(version) + " tessellation request"; + require(upgraded.request.graphics.has_value() && upgraded.request.graphics->tessellation.has_value() && !upgraded.request.graphics->tessellation->mergedHull, (what + " read a merged hull flag").c_str()); + require(serializer.Serialize(upgraded.request) == separate, (what + " misread the fields after its tessellation configuration").c_str()); + } + require(mergedKey != separateKey && RecompileCacheKey::ContextHash(request) != RecompileCacheKey::ContextHash(serializer.Deserialize(separate).request), "the cache keys ignore whether the hull program is merged"); +} + void verifyLegacyPixelRequests() { using namespace ShaderRecompiler; static constexpr std::array legacyPixelRequests{ @@ -5465,7 +5662,8 @@ void verifyComputedTexelOffsets() { request.useCache = false; AgcDriver::ShaderMemory memory({}); static_cast(memory.Capture(request)); - request.context.memory = memory.Regions(); + const auto memoryRegions = memory.Regions(); + request.context.memory = memoryRegions; return Recompile(request).spirv; }; const auto sampleOperands = [](std::span words) { @@ -5515,7 +5713,8 @@ void verifyShaderClockScopes() { request.useCache = false; AgcDriver::ShaderMemory memory({}); static_cast(memory.Capture(request)); - request.context.memory = memory.Regions(); + const auto memoryRegions = memory.Regions(); + request.context.memory = memoryRegions; const auto words = Recompile(request).spirv; std::map constants; std::vector scopeIds; @@ -5829,7 +6028,8 @@ void verifyUnnormalizedSamplers() { request.layout.pushConstantSizeBytes = 128; AgcDriver::ShaderMemory memory({}); static_cast(memory.Capture(request)); - request.context.memory = memory.Regions(); + const auto memoryRegions = memory.Regions(); + request.context.memory = memoryRegions; return Recompile(request); }; const auto flags = [](const RecompileResult& result, DescriptorRole role) { @@ -6252,7 +6452,8 @@ void verifyTwoLaneUniformValues() { request.layout.pushConstantSizeBytes = 128; AgcDriver::ShaderMemory memory({}); const auto capture = memory.Capture(request); - request.context.memory = memory.Regions(); + const auto memoryRegions = memory.Regions(); + request.context.memory = memoryRegions; const auto words = Recompile(request, *capture)->spirv; std::size_t count = 0; for (std::size_t cursor = 5; cursor < words.size();) { @@ -6728,6 +6929,10 @@ void verifySrtReadPatching() { const auto* replacement = repeatedUse.Argument(0); require(replacement->Opcode() == IrOpcode::ReadConst, "SRT read patching: the uses do not reference a flat read"); require(repeatedUse.Argument(1) == replacement && secondUser.Argument(0) == replacement, "SRT read patching: not every operand uses the same replacement"); + const std::vector order(block.Instructions().begin(), block.Instructions().end()); + const auto readAt = std::find(order.begin(), order.end(), &read); + require(readAt != order.end() && readAt - order.begin() >= 2 && *(readAt - 1) == replacement && *(readAt - 2) == replacement->Argument(0) && (*(readAt - 2))->Opcode() == IrOpcode::GetSrtResource, "SRT read patching: the flat read is not right before the original read"); + require(order.back()->Opcode() == IrOpcode::ReferenceU32 && order.back()->Argument(0) == &read, "SRT read patching: the planning reference is not at the end of the block"); const auto& remainingUses = read.OperandUses(); require(remainingUses.size() == 1u && remainingUses.front().user->Opcode() == IrOpcode::ReferenceU32, "SRT read patching: the original read retains non-planning uses"); } @@ -6789,6 +6994,161 @@ bool GuardRecords(const ShaderRecompiler::ResourceCapture& capture) { return true; } +void verifyGuardedDirectRoot() { + using namespace ShaderRecompiler; + constexpr std::size_t Reserved = 0x10000; + void* reserved = nullptr; +#ifdef _WIN32 + reserved = VirtualAlloc(nullptr, Reserved, MEM_RESERVE, PAGE_NOACCESS); +#else + reserved = mmap(nullptr, Reserved, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (reserved == MAP_FAILED) reserved = nullptr; +#endif + require(reserved != nullptr, "guarded direct root: cannot reserve an inaccessible range"); + const auto unmapped = static_cast(reinterpret_cast(reserved)); + alignas(256) static std::array output{}; + alignas(256) static std::array root{}; + static std::uint32_t payload = 0x3f800000u; + const auto outputAddress = static_cast(reinterpret_cast(output.data())); + const auto rootAddress = static_cast(reinterpret_cast(root.data())); + const auto payloadAddress = static_cast(reinterpret_cast(&payload)); + root = {static_cast(outputAddress), static_cast((outputAddress >> 32u) & 0xffffu), static_cast(sizeof(output)), 0x30027facu, 1u}; + const std::vector branchCode{0xf4080100u, 0xfa000000u, 0xf4000200u, 0xfa000010u, 0xbe8a0302u, 0xbe8b0303u, 0xbe8c03ffu, 0x11111111u, + 0xbf8cc07fu, 0xbf068008u, 0xbf850003u, 0xf4000305u, 0xfa000000u, 0xbf8cc07fu, 0x7e02020cu, 0x34040082u, 0xe0701000u, 0x80010102u, 0xbf810000u}; + for (const auto index : {4u, 5u}) { + const auto move = DecodeRdnaInstruction(index * 4u, branchCode, index); + require(move.op == RdnaOpcode::SMovB32 && move.destination.kind == RdnaOperandKind::ScalarRegister && move.destination.reg == index + 6u && + move.source0.kind == RdnaOperandKind::ScalarRegister && move.source0.reg == index - 2u, "guarded direct root: the fixture does not move the user-data pointer to s10:s11"); + } + const auto compare = DecodeRdnaInstruction(0x24u, branchCode, 9u); + require(compare.op == RdnaOpcode::SCmpEqU32 && compare.source0.kind == RdnaOperandKind::ScalarRegister && compare.source0.reg == 8u && + compare.source1.kind == RdnaOperandKind::IntegerInlineConstant && compare.source1.value == 0u, "guarded direct root: the fixture does not compare the branch word with zero"); + const auto condition = DecodeRdnaInstruction(0x28u, branchCode, 10u); + require(condition.op == RdnaOpcode::SCbranchScc1 && condition.branchTarget == 0x38u, "guarded direct root: the fixture does not skip the scalar load when the branch word is zero"); + NestedRequest branch(branchCode, root.data()); + std::array userData{static_cast(rootAddress), static_cast(rootAddress >> 32u), + static_cast(payloadAddress), static_cast(payloadAddress >> 32u)}; + branch.request.context.userData = userData; + AgcDriver::ShaderMemory validMemory({}); + const auto valid = validMemory.Capture(branch.request); + require(valid->snapshot.srtPoison.empty(), "guarded direct root: a mapped direct pointer was poisoned"); + const auto read = std::find_if(valid->plan->srtReads.begin(), valid->plan->srtReads.end(), [](const auto& candidate) { return candidate.value->template Flags().pc == 0x2cu; }); + require(read != valid->plan->srtReads.end(), "guarded direct root: the scalar read was not planned"); + require(!Detail::LoadedFromMemory(*read->value->Argument(0)), "guarded direct root: the pointer was classified as loaded from memory"); + require(valid->snapshot.flattenedSrt.at(read->flatOffset) == payload, "guarded direct root: the mapped scalar read has the wrong word"); + const auto validRegions = validMemory.Regions(); + branch.request.context.memory = validRegions; + const auto validResult = Recompile(branch.request, *valid); + require(validResult->poisonedSrtReads == 0u, "guarded direct root: the mapped program reports poison"); +#if ANYPS5_ENABLE_SPIRV_TOOLS + static_cast(ValidateAndOptimizeSpirv(validResult->spirv, branch.request.target.vulkanVersion, branch.request.target.spirvVersion)); +#endif + std::cout << "guarded direct root: mapped taken load passed\n"; + root[4] = 0u; + branch.request.context.memory = {}; + AgcDriver::ShaderMemory mappedSkippedMemory({}); + const auto mappedSkipped = mappedSkippedMemory.Capture(branch.request); + const auto mappedSkippedRegions = mappedSkippedMemory.Regions(); + branch.request.context.memory = mappedSkippedRegions; + const auto mappedSkippedResult = Recompile(branch.request, *mappedSkipped); + require(mappedSkippedResult->poisonedSrtReads == 0u, "guarded direct root: the mapped skipped program reports poison"); +#if ANYPS5_ENABLE_SPIRV_TOOLS + static_cast(ValidateAndOptimizeSpirv(mappedSkippedResult->spirv, branch.request.target.vulkanVersion, branch.request.target.spirvVersion)); +#endif + std::cout << "guarded direct root: mapped skipped load passed\n"; + root[4] = 1u; + userData[2] = static_cast(unmapped); + userData[3] = static_cast(unmapped >> 32u); + branch.request.context.memory = {}; + expectFailure([&] { + AgcDriver::ShaderMemory memory({}); + static_cast(memory.Capture(branch.request)); + }, "guest memory is not readable", "guarded direct root: a taken inaccessible load was accepted"); + std::cout << "guarded direct root: inaccessible taken load rejected\n"; + root[4] = 0u; + std::vector encodedPlan; + require(ShaderDiskCache::EncodePlan(*valid->plan, encodedPlan), "guarded direct root: cannot encode the resource plan"); + IrResourcePlan decodedPlan; + require(ShaderDiskCache::DecodePlan(encodedPlan, decodedPlan), "guarded direct root: cannot decode the resource plan"); + require(!valid->plan->controlFlow.empty() && decodedPlan.controlFlow.size() == valid->plan->controlFlow.size(), "guarded direct root: the scalar branch was not retained in the plan"); + for (std::size_t block = 0; block < decodedPlan.controlFlow.size(); ++block) { + require(decodedPlan.controlFlow[block].flatSlots == valid->plan->controlFlow[block].flatSlots, "guarded direct root: cached control flow lost its flat slots"); + } + IrResourcePlan decodedIrPlan; + require(ShaderDiskCache::DecodePlan(encodedPlan, decodedIrPlan), "guarded direct root: cannot decode the IR control plan"); + decodedIrPlan.compact = {}; + struct ReadContext { + std::uint64_t root; + std::uint64_t payload; + bool guardReadable; + std::uint32_t invalidReads = 0; + SrtMemoryReader reader = nullptr; + }; + const auto walk = [&](const IrResourcePlan& plan, bool tape, bool guardReadable) { + ReadContext context{rootAddress, payloadAddress, guardReadable}; + context.reader = [](void* opaque, std::uint64_t address, std::uint32_t* value) { + auto& context = *static_cast(opaque); + if (!((address >= context.root && address - context.root <= sizeof(root) - sizeof(*value)) || address == context.payload)) { + ++context.invalidReads; + return false; + } + std::memcpy(value, reinterpret_cast(address), sizeof(*value)); + return true; + }; + SrtRuntime runtime{userData}; + runtime.userContext = &context; + runtime.readMemory = context.reader; + runtime.readSpecializationMemory = [](void* opaque, std::uint64_t address, std::uint32_t* value) { + auto& context = *static_cast(opaque); + if (!context.guardReadable && address == context.root + 16u) return false; + return context.reader(opaque, address, value); + }; + const std::vector sourceCopy(plan.materializationSources.begin(), plan.materializationSources.end()); + const std::vector cleanCopy(plan.cleanFlatSlots.begin(), plan.cleanFlatSlots.end()); + const auto sources = tape ? std::span(plan.materializationSources) : std::span(sourceCopy); + const auto clean = tape ? std::span(plan.cleanFlatSlots) : std::span(cleanCopy); + std::vector descriptors; + std::vector flat; + std::vector active; + std::vector poison; + const auto* compiled = tape ? Detail::SrtTape::For(plan) : nullptr; + if (tape) require(compiled != nullptr, "guarded direct root: the plan has no SRT tape"); + const bool ok = tape ? compiled->Run(plan, runtime, descriptors, flat, active) : Detail::EvaluateRuntimeSourcesImpl(plan, sources, runtime, descriptors, flat, true, clean, active, &poison, nullptr); + require(ok == guardReadable, "guarded direct root: a known skipped load failed, or an unresolved guard silently succeeded"); + if (guardReadable) require(context.invalidReads == 0u && poison.empty(), "guarded direct root: the walker touched a known skipped load"); + }; + for (const IrResourcePlan* plan : {valid->plan.get(), static_cast(&decodedPlan), static_cast(&decodedIrPlan)}) { + for (const bool tape : {false, true}) { + walk(*plan, tape, true); + walk(*plan, tape, false); + } + } + auto repeatedCode = branchCode; + repeatedCode.insert(repeatedCode.begin() + 14, {0xf4000305u, 0xfa000000u, 0xbf8cc07fu}); + NestedRequest repeated(std::move(repeatedCode), root.data()); + repeated.request.context.userData = userData; + expectFailure([&] { + AgcDriver::ShaderMemory memory({}); + static_cast(memory.Capture(repeated.request)); + }, "guest memory is not readable", "guarded direct root: an unconditional repeated load was skipped with its guarded use"); + AgcDriver::ShaderMemory skippedMemory({}); + const auto skipped = skippedMemory.Capture(branch.request); + const auto skippedRegions = skippedMemory.Regions(); + require(skipped->snapshot.srtPoison.empty() && !RegionsTouch(skippedRegions, unmapped, 4u), "guarded direct root: a skipped scalar load was captured or poisoned"); + branch.request.context.memory = skippedRegions; + const auto skippedResult = Recompile(branch.request, *skipped); + require(skippedResult->poisonedSrtReads == 0u, "guarded direct root: the skipped program reports poison"); +#if ANYPS5_ENABLE_SPIRV_TOOLS + static_cast(ValidateAndOptimizeSpirv(skippedResult->spirv, branch.request.target.vulkanVersion, branch.request.target.spirvVersion)); +#endif + std::cout << "guarded direct root: inaccessible skipped load passed\n"; +#ifdef _WIN32 + VirtualFree(reserved, 0, MEM_RELEASE); +#else + munmap(reserved, Reserved); +#endif +} + void verifyGuardedNullPointers() { using namespace ShaderRecompiler; constexpr std::size_t Reserved = 0x10000; @@ -6826,6 +7186,28 @@ void verifyGuardedNullPointers() { const auto samePipeline = [](const RecompileResult& left, const RecompileResult& right) { return left.variantId == right.variantId && left.specializationId == right.specializationId && left.PipelineVariantId() == right.PipelineVariantId() && left.spirv == right.spirv; }; + const auto guardSites = [](const RecompileResult& result) { + const auto words = result.spirv.Words(); + std::map names; + std::map constants; + std::vector guardWords; + std::string function; + std::size_t calls = 0; + std::size_t stores = 0; + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> 16u; + const auto opcode = words[cursor] & 0xffffu; + require(count != 0 && count <= words.size() - cursor, "guarded pointer: truncated SPIR-V instruction"); + if (opcode == spv::OpName) names[words[cursor + 1]] = reinterpret_cast(&words[cursor + 2]); + if (opcode == spv::OpConstant && count == 4u) constants[words[cursor + 2]] = words[cursor + 3]; + if (opcode == spv::OpFunction) function = names[words[cursor + 2]]; + if (opcode == spv::OpAccessChain && count == 6u && names[words[cursor + 3]] == "fault_buffer" && constants.contains(words[cursor + 5]) && constants[words[cursor + 5]] == BdaAbi::SrtGuardWord) guardWords.push_back(words[cursor + 2]); + if (function == "main" && opcode == spv::OpFunctionCall && names[words[cursor + 3]] == "record_bda_fault") ++calls; + if (function == "main" && opcode == spv::OpAtomicStore && std::find(guardWords.begin(), guardWords.end(), words[cursor + 1]) != guardWords.end()) ++stores; + cursor += count; + } + return std::pair{calls, stores}; + }; NestedRequest branch(branchCode, root.data()); point(payloadAddress); @@ -6833,9 +7215,12 @@ void verifyGuardedNullPointers() { const auto valid = validMemory.Capture(branch.request); require(valid->snapshot.srtPoison.empty() && valid->plan->guardedSrtSlots.size() == 1u && slotPc(*valid->plan, valid->plan->guardedSrtSlots[0]) == 0x2cu && GuardRecords(*valid), "guarded pointer: a mapped nested pointer was poisoned, or its read has no guard"); require(valid->snapshot.flattenedSrt.at(valid->plan->guardedSrtSlots[0]) == payload, "guarded pointer: a mapped nested read does not hold its word"); - branch.request.context.memory = validMemory.Regions(); + const auto validRegions = validMemory.Regions(); + branch.request.context.memory = validRegions; const auto validResult = Recompile(branch.request, *valid); require(validResult->poisonedSrtReads == 0u && faultBinding(*validResult) && validResult->bdaAbiVersion == BdaAbi::Version, "guarded pointer: a guarded program has no fault buffer"); + const auto [branchCalls, branchStores] = guardSites(*validResult); + require(branchCalls == 0u && branchStores == 1u, ("guarded pointer: a guarded read calls record_bda_fault " + std::to_string(branchCalls) + " times in main and stores its record to the fault buffer's guard word " + std::to_string(branchStores) + " times, expected one store and no call").c_str()); for (const auto pointer : {std::uint64_t{0}, unmapped}) { for (const auto condition : {1u, 0u}) { @@ -6845,16 +7230,16 @@ void verifyGuardedNullPointers() { AgcDriver::ShaderMemory memory({}); const auto capture = memory.Capture(branch.request); const auto& poison = capture->snapshot.srtPoison; - require(poison.size() == 1u && poison[0].pc == 0x2cu && poison[0].address == pointer && slotPc(*capture->plan, poison[0].slot) == 0x2cu, "guarded pointer: the nested read was not poisoned at its pc and address"); + require(condition == 0u ? poison.empty() : poison.size() == 1u && poison[0].pc == 0x2cu && poison[0].address == pointer && slotPc(*capture->plan, poison[0].slot) == 0x2cu, "guarded pointer: a skipped nested read was poisoned, or an executed read has the wrong poison record"); require(GuardRecords(*capture), "guarded pointer: the poison record is not where the guard reads it"); const auto regions = memory.Regions(); require(!RegionsTouch(regions, pointer, 4u) && RegionsCover(regions, root.data() + 6, 8u), "guarded pointer: the captured regions are wrong"); branch.request.context.memory = regions; const auto result = Recompile(branch.request, *capture); - require(result->poisonedSrtReads == 1u && faultBinding(*result) && result->bdaAbiVersion == BdaAbi::Version, "guarded pointer: the poisoned capture does not report through the fault buffer"); + require(result->poisonedSrtReads == condition && faultBinding(*result) && result->bdaAbiVersion == BdaAbi::Version, "guarded pointer: the capture has the wrong poison count or no fault buffer"); require(samePipeline(*result, *validResult), "guarded pointer: poison changed the variant, the specialization or the module"); const auto replay = Recompile(branch.request); - require(replay.poisonedSrtReads == 1u && samePipeline(replay, *result), "guarded pointer: a replay of the captured regions did not reproduce the poison"); + require(replay.poisonedSrtReads == condition && samePipeline(replay, *result), "guarded pointer: a replay of the captured regions did not reproduce the poison"); verifyResult(*result, replay); #if ANYPS5_ENABLE_SPIRV_TOOLS static_cast(ValidateAndOptimizeSpirv(result->spirv, branch.request.target.vulkanVersion, branch.request.target.spirvVersion)); @@ -6875,22 +7260,27 @@ void verifyGuardedNullPointers() { const bool throughPointer = own == 0x38u && poison.address == unmapped; require(poison.pc == 0x2cu && (pointerWord || throughPointer), "guarded pointer: a derived poison does not name the inaccessible read"); } - chain.request.context.memory = chainMemory.Regions(); + const auto chainRegions = chainMemory.Regions(); + chain.request.context.memory = chainRegions; const auto chainResult = Recompile(chain.request, *chained); indirect = 0; point(static_cast(reinterpret_cast(&indirect))); AgcDriver::ShaderMemory nullChainMemory({}); const auto nullChain = nullChainMemory.Capture(chain.request); require(nullChain->snapshot.srtPoison.size() == 1u && nullChain->snapshot.srtPoison[0].pc == 0x38u && nullChain->snapshot.srtPoison[0].address == 0u && GuardRecords(*nullChain), "guarded pointer: a null pointer read from memory did not poison the read through it"); - chain.request.context.memory = nullChainMemory.Regions(); + const auto nullChainRegions = nullChainMemory.Regions(); + chain.request.context.memory = nullChainRegions; const auto nullChainResult = Recompile(chain.request, *nullChain); indirect = payloadAddress; AgcDriver::ShaderMemory mappedChainMemory({}); const auto mappedChain = mappedChainMemory.Capture(chain.request); require(mappedChain->snapshot.srtPoison.empty() && mappedChain->plan->guardedSrtSlots.size() == 3u && GuardRecords(*mappedChain), "guarded pointer: a mapped pointer chain was poisoned, or its reads have no guards"); - chain.request.context.memory = mappedChainMemory.Regions(); + const auto mappedChainRegions = mappedChainMemory.Regions(); + chain.request.context.memory = mappedChainRegions; const auto mappedChainResult = Recompile(chain.request, *mappedChain); require(chainResult->poisonedSrtReads == 3u && nullChainResult->poisonedSrtReads == 1u && mappedChainResult->poisonedSrtReads == 0u, "guarded pointer: a chain result does not count its poisoned reads"); + const auto [chainCalls, chainStores] = guardSites(*mappedChainResult); + require(chainCalls == 0u && chainStores == mappedChain->plan->guardedSrtSlots.size(), ("guarded pointer: a chain's guarded reads call record_bda_fault " + std::to_string(chainCalls) + " times in main and store to the guard word " + std::to_string(chainStores) + " times, expected one store per guard and no call").c_str()); require(samePipeline(*chainResult, *mappedChainResult) && samePipeline(*nullChainResult, *mappedChainResult), "guarded pointer: a chain's poison changed the variant, the specialization or the module"); const auto poisonedWords = [](const ResourceCapture& capture, std::uint32_t pc, std::uint64_t base) { @@ -6911,7 +7301,8 @@ void verifyGuardedNullPointers() { AgcDriver::ShaderMemory zeroMemory({}); const auto zeroCapture = zeroMemory.Capture(descriptor.request); require(zeroCapture->snapshot.srtPoison.empty() && zeroBuffer(*zeroCapture) && GuardRecords(*zeroCapture), "guarded pointer: a zero V# read from mapped memory was poisoned or not read"); - descriptor.request.context.memory = zeroMemory.Regions(); + const auto zeroRegions = zeroMemory.Regions(); + descriptor.request.context.memory = zeroRegions; const auto zeroResult = Recompile(descriptor.request, *zeroCapture); for (const auto pointer : {std::uint64_t{0}, unmapped}) { point(pointer); @@ -6953,11 +7344,11 @@ void verifyGuardedNullPointers() { vertex.request.context.memory = {}; AgcDriver::ShaderMemory memory({}); const auto capture = memory.Capture(vertex.request); - require(poisonedWords(*capture, 0x20u, 0x10u), "guarded pointer: a vertex V# behind a branch and a null pointer did not poison its four words at its pc"); + require(guard == 0u ? capture->snapshot.srtPoison.empty() && GuardRecords(*capture) : poisonedWords(*capture, 0x20u, 0x10u), "guarded pointer: a skipped vertex V# was poisoned, or an executed read has the wrong poison record"); const auto regions = memory.Regions(); vertex.request.context.memory = regions; const auto result = Recompile(vertex.request, *capture); - require(result->poisonedSrtReads == 4u && faultBinding(*result) && !result->spirv.empty(), "guarded pointer: a vertex capture with a poisoned V# has no fault buffer"); + require(result->poisonedSrtReads == guard * 4u && faultBinding(*result) && !result->spirv.empty(), "guarded pointer: a vertex capture has the wrong poison count or no fault buffer"); #if ANYPS5_ENABLE_SPIRV_TOOLS static_cast(ValidateAndOptimizeSpirv(result->spirv, vertex.request.target.vulkanVersion, vertex.request.target.spirvVersion)); #endif @@ -6967,7 +7358,8 @@ void verifyGuardedNullPointers() { const auto rootlessCapture = rootlessMemory.Capture(rootless.request); require(rootlessCapture->snapshot.nullRootReads != 0u && GuardRecords(*rootlessCapture), "guarded pointer: reads through a null user-data pointer were not counted"); require(std::all_of(rootlessCapture->snapshot.buffers.begin(), rootlessCapture->snapshot.buffers.end(), [](const DescriptorValue& value) { return std::all_of(value.dwords.begin(), value.dwords.end(), [](std::uint32_t word) { return word == 0u; }); }), "guarded pointer: a V# read through a null user-data pointer is not zero"); - rootless.request.context.memory = rootlessMemory.Regions(); + const auto rootlessRegions = rootlessMemory.Regions(); + rootless.request.context.memory = rootlessRegions; const auto rootlessResult = Recompile(rootless.request, *rootlessCapture); require(rootlessResult->poisonedSrtReads >= rootlessCapture->snapshot.nullRootReads, "guarded pointer: a capture through a null user-data pointer is cacheable"); point(0u); @@ -7081,6 +7473,13 @@ void verifyVolumeArrayLoadMode() { require(modes[selected].dimension == RdnaImageDimension::Dim3D && !modes[selected].cube, "2D-array image load on a 3D T# did not select a volume runtime mode"); image.fmaskCompatible = false; expectFailure([&] { static_cast(ResourceMaterializer::RuntimeImageMode(image, descriptor, ResourceMaterializer::RuntimeImageModes(image))); }, "incompatible with the static runtime image interface", "2D-array image sample on a 3D T# was accepted"); + image.volumeArraySampleCompatible = true; + const auto sampleModes = ResourceMaterializer::RuntimeImageModes(image); + const auto& sampled = sampleModes[ResourceMaterializer::RuntimeImageMode(image, descriptor, sampleModes)]; + require(sampled.dimension == RdnaImageDimension::Dim3D && !sampled.cube, "2D-array image sample on a 3D T# did not select a volume runtime mode"); + const DescriptorValue arrayDescriptor{{0x02466600u, 0x01400000u, 0u, 0xd0500204u, 1u, 0x00700000u, 0u, 0u}, 8u}; + require(sampleModes[ResourceMaterializer::RuntimeImageMode(image, arrayDescriptor, sampleModes)].dimension == RdnaImageDimension::Dim2DArray, "2D-array image sample on a 2D-array T# no longer selects the 2D-array runtime mode"); + image.volumeArraySampleCompatible = false; ImageResource store{}; store.resourceClass = ImageResourceClass::Storage; store.dimension = RdnaImageDimension::Dim2DArray; @@ -7222,6 +7621,7 @@ int main(int argc, char** argv) { verifySelectedRecordUniformIndex(); verifyChainedTableOffset(); verifyPointerChainImageTable(); + verifyWaterfallImageTable(); std::cout << "Image table keys, maps, records and registration passed\n"; return 0; } @@ -7284,6 +7684,7 @@ int main(int argc, char** argv) { verifyPointerTableOffsetCarry(); verifyChainedTableOffset(); verifyPointerChainImageTable(); + verifyWaterfallImageTable(); verifyDescriptorPhis(); verifyUniformTableImage(); verifyLaneSelectedDescriptors(); @@ -7298,6 +7699,7 @@ int main(int argc, char** argv) { verifyPixelRequestSerialization(); verifyVertexInfoVersion19(); verifyDepthRemapRequests(); + verifyMergedHullConfiguration(); verifyPositionExportComponents(); verifyLegacyPixelRequests(); verifyPixelExportReplay(); @@ -7318,6 +7720,7 @@ int main(int argc, char** argv) { verifyFunctionLdsBound(); verifyFunctionLdsUpdates(); verifySrtReadPatching(); + verifyGuardedDirectRoot(); verifyGuardedNullPointers(); verifyFmaskImageMode(); verifyCubeDepthCompareMode(); @@ -7449,6 +7852,13 @@ int main(int argc, char** argv) { std::vector float32FlushKey; RecompileCacheKey::Build(float32Flush, float32FlushKey); require(plainKey != float32FlushKey, "f32 hardware flush did not change the cache key"); + auto float32Preserve = uncached; + float32Preserve.target.float32DenormPreserve = true; + require(RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(float32Preserve)).request.target.float32DenormPreserve, "f32 denormal preservation was lost in serialization"); + require(!RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(uncached)).request.target.float32DenormPreserve, "f32 denormal preservation appeared from nowhere in serialization"); + std::vector float32PreserveKey; + RecompileCacheKey::Build(float32Preserve, float32PreserveKey); + require(plainKey != float32PreserveKey && float32FlushKey != float32PreserveKey, "f32 denormal preservation did not change the cache key"); auto changedLayout = request; changedLayout.layout.pushConstantSizeBytes = 64; require(!Recompile(changedLayout).cacheHit, "binding layout change reused an incompatible variant"); @@ -7499,7 +7909,8 @@ int main(int argc, char** argv) { const auto changedCapture = changedMemory.Capture(request); auto changed = updated; changed.useCache = true; - changed.context.memory = changedMemory.Regions(); + const auto changedRegions = changedMemory.Regions(); + changed.context.memory = changedRegions; auto before = updated; before.useCache = true; before.context.memory = updatedRegions; @@ -7562,7 +7973,8 @@ int main(int argc, char** argv) { AgcDriver::ShaderMemory nullRoot({}); const auto nullRootCapture = nullRoot.Capture(request); require(nullRootCapture->snapshot.nullRootReads != 0u, "reads through a null user-data pointer were not counted"); - request.context.memory = nullRoot.Regions(); + const auto nullRootRegions = nullRoot.Regions(); + request.context.memory = nullRootRegions; require(Recompile(request, *nullRootCapture)->poisonedSrtReads >= nullRootCapture->snapshot.nullRootReads, "a capture through a null user-data pointer is cacheable"); std::cout << "Shader memory capture, strict validation and deterministic replay passed\n"; return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/SlabReturn.hpp b/core/libs/prx/libSceAgcDriver/tests/SlabReturn.hpp new file mode 100644 index 0000000000..3298f86e59 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/SlabReturn.hpp @@ -0,0 +1,80 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_SLABRETURN_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_SLABRETURN_HPP + +#include "AllocationFailure.hpp" +#include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" +#include +#include +#include +#include +#include +#include + +namespace AgcDriver::Tests { + +inline void SlabReturnWithoutAllocation(const Graphics::Context& context, std::uint32_t memoryType) { + constexpr std::size_t capacity = std::size_t{512} << 10u; + constexpr std::size_t slotsPerBlock = 8; + Graphics::Require(Graphics::BufferPool::SlabBlockBytes(capacity) == capacity * slotsPerBlock, "unexpected slab size in the return test"); + Graphics::BufferPool pool(context); + std::array slots{}; + std::array reused{}; + unsigned returned = 0; + for (const std::size_t blocks : {4u, 6u, 3u}) { + std::printf("slab return type=%u blocks=%zu filling\n", memoryType, blocks); + std::fflush(stdout); + const auto count = blocks * slotsPerBlock; + for (std::size_t index = 0; index < count; ++index) { + slots[index] = pool.TakeSlot(context, memoryType, capacity, false); + if (slots[index].mapping != nullptr) std::memcpy(slots[index].mapping, &index, sizeof(index)); + } + Graphics::Require(pool.SlabBlocks() == blocks, "the return test did not fill every slab"); + std::puts("slab return arming host allocation failure"); + std::fflush(stdout); + const auto returnSlots = [&] { + const auto previous = std::set_terminate([] { + std::fputs("FAIL: slab return terminated during host allocation failure\n", stderr); + std::fflush(stderr); + std::_Exit(1); + }); + SetAllocationFailureCountdown(0); + for (std::size_t block = 0; block < blocks; ++block) pool.PutSlot(reused[block].memory, reused[block].offset); + SetAllocationFailureCountdown(-1); + std::set_terminate(previous); + }; + for (std::size_t block = 0; block < blocks; ++block) reused[block] = slots[block * slotsPerBlock]; + returnSlots(); + for (std::size_t block = 0; block < blocks; ++block) { + const auto index = block * slotsPerBlock + 1; + if (slots[index].mapping == nullptr) continue; + std::size_t value = 0; + std::memcpy(&value, slots[index].mapping, sizeof(value)); + Graphics::Require(value == index, "returning a slot changed a live sibling"); + } + std::array seen{}; + for (std::size_t block = 0; block < blocks; ++block) { + reused[block] = pool.TakeSlot(context, memoryType, capacity, false); + bool matched = false; + for (std::size_t candidate = 0; candidate < blocks; ++candidate) { + const auto& expected = slots[candidate * slotsPerBlock]; + if (reused[block].memory != expected.memory || reused[block].offset != expected.offset) continue; + Graphics::Require(!seen[candidate], "a returned slot was duplicated"); + seen[candidate] = true; + matched = true; + } + Graphics::Require(matched, "a returned slot was lost"); + } + returnSlots(); + VkDeviceSize released = 0; + for (std::size_t index = 0; index < count; ++index) { + if (index % slotsPerBlock != 0) released += pool.PutSlot(slots[index].memory, slots[index].offset); + } + Graphics::Require(pool.SlabBlocks() == 1 && released == (blocks - 1) * capacity * slotsPerBlock, "empty slab retirement retained the wrong number of blocks"); + returned += static_cast(blocks * 2); + } + std::printf("slab return type=%u cycles=3 returned=%u host allocation failures armed\n", memoryType, returned); +} + +} + +#endif 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/SpirvLdsAtomicLock.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvLdsAtomicLock.cpp index da606bd9a3..8155564e0b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SpirvLdsAtomicLock.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvLdsAtomicLock.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -27,9 +28,12 @@ struct Usage { std::size_t elects = 0; std::size_t exchanges = 0; std::size_t releases = 0; + std::size_t storeReleases = 0; std::size_t adds = 0; bool addBetweenLock = false; bool lockInitIsLaneZero = false; + bool fenceBetweenLock = false; + bool releaseWithoutReleaseSemantics = false; }; void Require(bool condition, const std::string& message) { @@ -72,6 +76,7 @@ Usage Scan(std::span words) { std::uint32_t laneZeroCondition = 0u; std::uint32_t laneZeroBody = 0u; std::uint32_t lockPointer = 0u; + std::map constants; for (std::size_t cursor = 5; cursor < words.size();) { const auto count = words[cursor] >> spv::WordCountShift; const auto op = static_cast(words[cursor] & spv::OpCodeMask); @@ -86,16 +91,24 @@ Usage Scan(std::span words) { laneZeroCondition = words[cursor + 2u]; } if (op == spv::OpBranchConditional && count >= 4u && words[cursor + 1u] == laneZeroCondition) laneZeroBody = words[cursor + 2u]; + if (op == spv::OpConstant && count == 4) constants[words[cursor + 2]] = words[cursor + 3]; if (op == spv::OpGroupNonUniformElect) ++usage.elects; if (op == spv::OpAtomicCompareExchange) { ++usage.exchanges; locked = true; if (count >= 4u) lockPointer = words[cursor + 3u]; } - if (op == spv::OpAtomicStore) { + if (op == spv::OpAtomicExchange) { ++usage.releases; + const auto semantics = constants.find(words[cursor + 5]); + if (semantics == constants.end() || (semantics->second & spv::MemorySemanticsReleaseMask) == 0) usage.releaseWithoutReleaseSemantics = true; locked = false; } + if (op == spv::OpAtomicStore) { + ++usage.storeReleases; + locked = false; + } + if (op == spv::OpMemoryBarrier && locked) usage.fenceBetweenLock = true; if (op == spv::OpAtomicIAdd) { ++usage.adds; if (locked) usage.addBetweenLock = true; @@ -146,22 +159,27 @@ void Check64BitOnlyLocks() { const auto usage = Compile({DsAddU64, DsOperands}); Require(usage.elects == 1 && usage.exchanges == 1 && usage.releases == 1, "a ds_add_u64 is not serialised by one elected lane taking the workgroup lock"); Require(usage.lockInitIsLaneZero, "the workgroup lock is not initialized by only local invocation zero"); + Require(usage.storeReleases == 0 && !usage.releaseWithoutReleaseSemantics, "the workgroup lock is not released by an exchange with release semantics: atomic stores " + std::to_string(usage.storeReleases)); + Require(!usage.fenceBetweenLock, "a ds_add_u64 emits a memory barrier while it holds the workgroup lock"); } void Check32BitTakesLockWith64Bit() { const auto usage = Compile({DsAddU64, DsOperands, DsAddU32Hi, DsOperand32}); Require(usage.elects == 2 && usage.exchanges == 2 && usage.releases == 2, "ds_add_u32 in a program with ds_add_u64 does not take the workgroup lock: elect " + std::to_string(usage.elects) + ", lock acquire " + std::to_string(usage.exchanges) + ", lock release " + std::to_string(usage.releases) + ", expected 2 each"); Require(usage.adds == 1 && usage.addBetweenLock, "the 32-bit OpAtomicIAdd is not between the lock acquire and the lock release"); + Require(!usage.fenceBetweenLock, "ds_add_u32 emits a memory barrier while it holds the workgroup lock"); } void CheckOtherUpdatePathTakesLock() { const auto usage = Compile({DsAddU64, DsOperands, DsIncU32Hi, DsOperand32}); Require(usage.elects == 2 && usage.exchanges >= 2 && usage.releases == 2, "ds_inc_u32 in a program with ds_add_u64 does not take the workgroup lock: elect " + std::to_string(usage.elects) + ", lock release " + std::to_string(usage.releases) + ", expected 2 each"); + Require(!usage.fenceBetweenLock, "ds_inc_u32 emits a memory barrier while it holds the workgroup lock"); } void CheckFlatAtomicTakesLockWith64Bit() { const auto usage = Compile({DsAddU64, DsOperands, FlatAtomicAddU32, FlatAtomicOperands}); Require(usage.elects == 2, "a 32-bit flat atomic into the LDS aperture in a program with ds_add_u64 does not take the workgroup lock: elect " + std::to_string(usage.elects) + ", expected 2"); + Require(!usage.fenceBetweenLock, "a 32-bit flat atomic into the LDS aperture emits a memory barrier while it holds the workgroup lock"); } void CheckFlatAtomicOnlyIsUnchanged() { 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/SubgroupBarrier.cpp b/core/libs/prx/libSceAgcDriver/tests/SubgroupBarrier.cpp new file mode 100644 index 0000000000..272e5ed914 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/SubgroupBarrier.cpp @@ -0,0 +1,110 @@ +#include "prx/libSceAgcDriver/Execution/include/SubgroupBarrier.hpp" +#include "Recompiler.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +struct Case { + VkDriverId driver; + bool usable; +}; + +constexpr Case Cases[]{ + {VK_DRIVER_ID_AMD_PROPRIETARY, false}, + {VK_DRIVER_ID_AMD_OPEN_SOURCE, true}, + {VK_DRIVER_ID_MESA_RADV, true}, + {VK_DRIVER_ID_NVIDIA_PROPRIETARY, true}, + {VK_DRIVER_ID_INTEL_PROPRIETARY_WINDOWS, true}, + {VK_DRIVER_ID_INTEL_OPEN_SOURCE_MESA, true}, + {VK_DRIVER_ID_MESA_LLVMPIPE, true}, + {VK_DRIVER_ID_MOLTENVK, true}, +}; + +constexpr std::array BranchedLdsStep{ + 0x34040082u, 0x4a020081u, 0xd8340000u, 0x00000102u, 0xbf8cc07fu, 0xbf8a0000u, 0x7da800ffu, 0x00000080u, + 0xbf880006u, 0xd8d80200u, 0x03000002u, 0xbf8cc07fu, 0x4a020701u, 0xd8340000u, 0x00000102u, 0xbefe03c1u, + 0xbf8cc07fu, 0xbf8a0000u, 0xbf810000u, +}; + +struct Barriers { + std::uint32_t subgroupControl = 0; + std::uint32_t subgroupMemory = 0; +}; + +void require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +Barriers recompile(std::uint32_t threads, bool waveLdsMemoryBarriers) { + using namespace ShaderRecompiler; + const std::array capabilities{spv::CapabilityShader, spv::CapabilityGroupNonUniform}; + const std::array extensions{"SPV_KHR_storage_buffer_storage_class"}; + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, 0x20000u, BranchedLdsStep, 0, {}}; + request.context.waveSize = 32; + request.context.compute = ShaderComputeStageInfo{{threads, 1u, 1u}, 256u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00403000u; + request.target.spirvVersion = 0x00010600u; + request.target.subgroupSize = 32; + 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.waveLdsMemoryBarriers = waveLdsMemoryBarriers; + request.layout = {0, 0, 0, 128}; + request.useCache = false; + const auto result = Recompile(request); + const auto& words = result.spirv.Words(); + std::unordered_map constants; + Barriers barriers; + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + const auto op = static_cast(words[at] & 0xffffu); + if (op == spv::OpConstant && (words[at] >> 16u) == 4u) constants[words[at + 2]] = words[at + 3]; + if (op != spv::OpControlBarrier && op != spv::OpMemoryBarrier) continue; + const auto scope = constants.find(words[at + 1]); + require(scope != constants.end(), "a barrier scope is not a 32-bit constant"); + if (scope->second != spv::ScopeSubgroup) continue; + if (op == spv::OpControlBarrier) ++barriers.subgroupControl; + else ++barriers.subgroupMemory; + } + return barriers; +} + +void verifyDriverQuirk() { + for (const auto& item : Cases) { + require(AgcDriver::SubgroupControlBarrierUsable(item.driver) == item.usable, "driver " + std::to_string(static_cast(item.driver)) + ": expected subgroup control barriers to be " + (item.usable ? "usable" : "unusable")); + } +} + +void verifyEmittedBarriers() { + const auto control = recompile(256, false); + require(control.subgroupControl != 0 && control.subgroupMemory == 0, "a wave32 program in a workgroup of eight waves did not order its LDS accesses with subgroup-scope OpControlBarrier only"); + const auto memory = recompile(256, true); + require(memory.subgroupControl == 0 && memory.subgroupMemory != 0, "with wave LDS memory barriers, a wave32 program in a workgroup of eight waves did not order its LDS accesses with subgroup-scope OpMemoryBarrier only"); + const auto single = recompile(32, true); + require(single.subgroupControl == 0 && single.subgroupMemory == 0, "with wave LDS memory barriers, a single-wave workgroup did not keep workgroup-scope barriers"); +} + +} + +int main() { + try { + verifyDriverQuirk(); + verifyEmittedBarriers(); + std::puts("subgroup barrier tests passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp index 5549ae9fb1..d97862941b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp @@ -4,6 +4,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/SynchronizationStatistics.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include "prx/libc/include/GuestHeap.hpp" #include "prx/libc/include/Shutdown.hpp" #include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" @@ -573,6 +574,49 @@ void testCopyIntoHostMemory() { GuestHeap::GuestHeapFree_nid_postfix(produced); } +void testSmallCopyBehindPendingSource() { + alignas(64) static volatile std::uint32_t copied = 0, settledCopied = 0, hostCopied = 0; + constexpr std::size_t words = 32768; + constexpr auto bytes = static_cast(words * 4); + constexpr std::size_t smallWords = 1024; + constexpr auto smallBytes = static_cast(smallWords * 4); + auto* produced = static_cast(GuestHeap::GuestHeapAlignForGpu_nid_postfix(65536, bytes)); + auto* source = static_cast(GuestHeap::GuestHeapAlignForGpu_nid_postfix(65536, bytes)); + auto* destination = static_cast(GuestHeap::GuestHeapAlignForGpu_nid_postfix(65536, smallBytes)); + for (std::size_t i = 0; i < words; ++i) { + produced[i] = 0xc3000000u | static_cast(i); + source[i] = 0x3c000000u | static_cast(i); + } + std::fill(destination, destination + smallWords, 0u); + const auto before = AgcDriver::DriverDetail::copiesBehindPendingSources.load(); + submit(0, commands(memoryCopy(source, produced, bytes), memoryCopy(destination, source, smallBytes), writeData(&copied, 1))); + waitFor(&copied, 1, "the label after a small copy behind a pending source never landed"); + AgcDriverWaitIdle_nid_postfix(); + check(std::equal(destination, destination + smallWords, produced), "a small copy did not see the GPU copy into its source before it"); + if (std::getenv("APS5_COPY_SYNC") != nullptr) check(AgcDriver::DriverDetail::copiesBehindPendingSources.load() == before, "a small copy that has to wait for a completion store over its source went through the GPU copy"); + else check(AgcDriver::DriverDetail::copiesBehindPendingSources.load() == before + 1, "a small copy whose source the GPU still stores into was not recorded on the GPU"); + std::fill(destination, destination + smallWords, 0u); + const auto settled = AgcDriver::DriverDetail::copiesBehindPendingSources.load(); + submit(0, commands(memoryCopy(destination, produced, smallBytes), writeData(&settledCopied, 1))); + waitFor(&settledCopied, 1, "the label after a small copy of a settled source never landed"); + AgcDriverWaitIdle_nid_postfix(); + check(std::equal(destination, destination + smallWords, produced), "a small copy of a settled source lost bytes"); + check(AgcDriver::DriverDetail::copiesBehindPendingSources.load() == settled, "a small copy of a settled source was recorded on the GPU"); + std::vector hostDestination(smallWords); + for (std::size_t i = 0; i < words; ++i) source[i] = 0x3c000000u | static_cast(i); + const auto hostCopies = AgcDriver::DriverDetail::copiesBehindPendingSources.load(); + const auto aliasChecks = AgcDriver::VulkanDevice::CopyAliasCounts().noSource; + submit(0, commands(memoryCopy(source, produced, bytes), memoryCopy(hostDestination.data(), source, smallBytes), writeData(&hostCopied, 1))); + waitFor(&hostCopied, 1, "the label after a small copy into memory that is not imported never landed"); + AgcDriverWaitIdle_nid_postfix(); + check(std::equal(hostDestination.begin(), hostDestination.end(), produced), "a small copy into memory that is not imported did not see the GPU copy into its source"); + check(AgcDriver::DriverDetail::copiesBehindPendingSources.load() == hostCopies, "a small copy into memory that is not imported was recorded on the GPU"); + check(AgcDriver::VulkanDevice::CopyAliasCounts().noSource == aliasChecks + 1, "a small copy into memory that is not imported went through the GPU copy before its CPU path"); + GuestHeap::GuestHeapFree_nid_postfix(destination); + GuestHeap::GuestHeapFree_nid_postfix(source); + GuestHeap::GuestHeapFree_nid_postfix(produced); +} + void testShaderHeaderAlignment() { alignas(256) static const std::array code{0xbf810000}; struct Header { @@ -881,6 +925,13 @@ int main(int argc, char** argv) { } const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; + if (argc == 2 && std::string_view(argv[1]) == "--small-copy-sync") { + testHeaderWithoutProgramAddress(); + testSmallCopyBehindPendingSource(); + LibcRunShutdown_nid_postfix(); + std::puts("Small copies with APS5_COPY_SYNC passed"); + return 0; + } alignas(256) std::array rawCode{}; rawCode.fill(0xbf800000); rawCode[0] = 0xbe8003ff; @@ -975,6 +1026,7 @@ int main(int argc, char** argv) { testHeaderWithoutProgramAddress(); testCopyIntoHostMemory(); testLargeDataFill(); + testSmallCopyBehindPendingSource(); testRegisteredFloatMode(); testDispatchFloatMode(); testReadOnlyResummarize(); 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/TessellationLowering.cpp b/core/libs/prx/libSceAgcDriver/tests/TessellationLowering.cpp index 287ca9bfd8..1b1ec2d0da 100644 --- a/core/libs/prx/libSceAgcDriver/tests/TessellationLowering.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/TessellationLowering.cpp @@ -102,11 +102,12 @@ struct Outcome { }; const ShaderTessellationInputInfo Tessellation{.inputControlPoints = InputControlPoints, .outputControlPoints = InputControlPoints, .patchesPerGroup = 1u}; +const ShaderTessellationInputInfo GroupedTessellation{.inputControlPoints = InputControlPoints, .outputControlPoints = InputControlPoints, .patchesPerGroup = 1u, .primitiveGroupSize = 1u}; -Outcome Lower(IrProgram& program) { +Outcome Lower(IrProgram& program, const ShaderTessellationInputInfo& tessellation = Tessellation) { Outcome outcome; try { - TessellationLowering{}.Lower(program, Tessellation); + TessellationLowering{}.Lower(program, tessellation); } catch (const std::exception& error) { outcome.reason = error.what(); return outcome; @@ -193,5 +194,17 @@ int main() { return RefusedBecause(hull.program, "not a whole number of control points"); }); + passed &= run("a hull read at a computed control-point index stays in its patch when the relative patch is also data", [] { + Hull hull; + auto& address = hull.inPatch(hull.computed(2u, LocalStride)); + hull.read(address); + static_cast(hull.builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&hull.relativePatch()})); + const Outcome outcome = Lower(hull.program, GroupedTessellation); + if (!outcome.lowered) std::fprintf(stderr, "refused: %s\n", outcome.reason.c_str()); + if (!outcome.lowered || outcome.emitted->Opcode() != IrOpcode::ISub32 || outcome.emitted->Argument(0)->Resolve() != address.Resolve()) return false; + const IrValue* patch = outcome.emitted->Argument(1)->Resolve(); + return patch->Opcode() == IrOpcode::IMul32 && patch->Argument(1)->Resolve()->ImmediateU32() == PatchStride; + }); + return passed ? 0 : 1; } diff --git a/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp b/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp index bf25b268ee..31ff43033e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp @@ -263,6 +263,8 @@ void RunTextureFormatTests() { reject([] { IsBlockCompressed(200); }, "unsupported guest texture format"); for (const std::uint32_t format : {128u, 129u, 130u, 170u, 172u, 174u, 182u}) Require(IsSrgbTextureFormat(format), "guest format " + std::to_string(format) + " must be an sRGB texture format"); for (const std::uint32_t format : {1u, 14u, 56u, 71u, 169u, 171u, 173u, 181u}) Require(!IsSrgbTextureFormat(format), "guest format " + std::to_string(format) + " must not be an sRGB texture format"); + for (const std::uint32_t format : {7u, 13u, 22u, 36u, 50u, 77u, 132u, 133u, 175u, 179u}) Require(IgnoresForcedDegamma(format), "guest format " + std::to_string(format) + " must sample the same through FORCE_DEGAMMA"); + for (const std::uint32_t format : {1u, 2u, 14u, 15u, 56u, 57u, 128u, 130u, 169u, 171u, 173u, 181u}) Require(!IgnoresForcedDegamma(format), "guest format " + std::to_string(format) + " must not sample the same through FORCE_DEGAMMA"); convertedDccClearTests(); singleDccKeysTests(); packedFloatDccClearTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp b/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp index 6c6acca9d2..cb439c6ffe 100644 --- a/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp @@ -388,4 +388,99 @@ void RunTextureTilingTests() { reject([] { ComputeSurfaceSize({}, 1); }, "empty mip chain"); reject([] { ComputeSurfaceSize(ComputeMipLayout(TextureTileMode::kLinear, 1, 4, 4, 1), 0); }, "zero array layers"); + + { + GuestTextureResource array{}; + array.baseAddress = 0x10000000ull; + array.width = 3328; + array.height = 1872; + array.format = 1; + array.mipCount = 2; + array.dimension = TextureDimension::k2DArray; + array.tileMode = TextureTileMode::kR64KBX; + array.depthOrLastArray = 1536; + array.baseArray = 1536; + const auto layerBytes = DescribeSurface(array).layerBytes; + const auto window = ViewedArrayWindow(array); + Require(window.baseAddress == array.baseAddress + 1536ull * layerBytes && window.depthOrLastArray == 0 && window.baseArray == 0, "a sampled view of one slice at 1536 of an R_X array must start at that slice"); + Require(DescribeSurface(window).layerBytes == layerBytes && DescribeSurface(window).layers == 1, "a sampled array window must keep the slice layout"); + + auto unaligned = array; + unaligned.baseArray = 1530; + Require(ViewedArrayWindow(unaligned).baseAddress == array.baseAddress, "an R_X window at a slice the swizzle mixes in must keep the whole array"); + + auto plain = array; + plain.tileMode = TextureTileMode::kStandard64KB; + plain.baseArray = 1530; + Require(ViewedArrayWindow(plain).baseAddress == array.baseAddress + 1530ull * DescribeSurface(plain).layerBytes && ViewedArrayWindow(plain).depthOrLastArray == 6, "a non-XOR window may start at any slice"); + + auto fromZero = array; + fromZero.baseArray = 0; + Require(ViewedArrayWindow(fromZero).baseAddress == array.baseAddress && ViewedArrayWindow(fromZero).depthOrLastArray == 1536, "a view from slice 0 keeps its descriptor"); + + auto compressed = array; + compressed.dccAddress = 0x20000000ull; + Require(ViewedArrayWindow(compressed).baseAddress == array.baseAddress, "a view with DCC metadata keeps its descriptor"); + + GuestTextureResource cube{}; + cube.baseAddress = 0x30000000ull; + cube.width = 256; + cube.height = 256; + cube.format = 71; + cube.mipCount = 1; + cube.dimension = TextureDimension::kCube; + cube.tileMode = TextureTileMode::kStandard4KB; + cube.depthOrLastArray = 7232; + cube.baseArray = 7227; + const auto faces = ViewedArrayWindow(cube); + Require(faces.baseAddress == cube.baseAddress + 7227ull * DescribeSurface(cube).layerBytes && faces.depthOrLastArray == 5 && faces.baseArray == 0 && faces.dimension == TextureDimension::kCube, "a cube view of six faces at 7227 must start at its first face"); + + auto volume = cube; + volume.dimension = TextureDimension::k3D; + 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/VectorWindow.cpp b/core/libs/prx/libSceAgcDriver/tests/VectorWindow.cpp index 2028dcb2b1..a69e325898 100644 --- a/core/libs/prx/libSceAgcDriver/tests/VectorWindow.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/VectorWindow.cpp @@ -3,6 +3,7 @@ #include "RdnaDecoder/RdnaInstructionDecoder.hpp" #include "Translation/InstructionTranslator.hpp" #include +#include #include #include #include @@ -21,6 +22,7 @@ using namespace ShaderRecompiler; constexpr std::uint32_t MoveM0Two = 0xbefc0382u; constexpr std::uint32_t MoveM0GroupX = 0xbefc0304u; constexpr std::uint32_t MoveV4One = 0x7e0802f2u; +constexpr std::uint32_t MoveV4Two = 0x7e0802f4u; constexpr std::uint32_t MoveV5Two = 0x7e0a02f4u; constexpr std::uint32_t MoveV6Four = 0x7e0c02f6u; constexpr std::uint32_t MovrelsV1V4 = 0x7e028704u; @@ -117,6 +119,24 @@ Shape Scan(std::span words) { return shape; } +std::size_t WindowStores(std::span words) { + std::vector functionPointers; + std::vector pointers; + std::size_t stores = 0; + const auto contains = [](const std::vector& ids, std::uint32_t id) { return std::find(ids.begin(), ids.end(), id) != ids.end(); }; + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> spv::WordCountShift; + const auto op = static_cast(words[cursor] & spv::OpCodeMask); + Require(count != 0 && cursor + count <= words.size(), "truncated SPIR-V instruction"); + if (op == spv::OpTypePointer && words[cursor + 2] == spv::StorageClassFunction) functionPointers.push_back(words[cursor + 1]); + if (op == spv::OpVariable && words[cursor + 3] == spv::StorageClassFunction && contains(functionPointers, words[cursor + 1])) pointers.push_back(words[cursor + 2]); + if (op == spv::OpAccessChain && contains(pointers, words[cursor + 3])) pointers.push_back(words[cursor + 2]); + if (op == spv::OpStore && contains(pointers, words[cursor + 1])) ++stores; + cursor += count; + } + return stores; +} + IrProgram TranslateStage(ShaderStageKind stage, std::uint32_t waveSize, std::uint32_t base = 0u, std::uint32_t fetchStart = 0u, std::uint32_t fetchCount = 0u) { ShaderComputeInputInfo compute{}; compute.threadIdsNum = 3; @@ -293,6 +313,15 @@ int main(int argc, char** argv) { return shape.functionArrays == 1; }); + passed &= run("a window register written twice before a relative read keeps only its last window store", [] { + const std::array overwritten{MoveM0GroupX, MoveV4One, MoveV4Two, MoveV5Two, MoveV6Four, MovrelsV1V4, 0xe0700000u, 0x80000100u, EndProgram}; + const std::array once{MoveM0GroupX, MoveV4Two, MoveV5Two, MoveV6Four, MovrelsV1V4, 0xe0700000u, 0x80000100u, EndProgram}; + const auto twice = WindowStores(RecompileProgram(overwritten)); + const auto single = WindowStores(RecompileProgram(once)); + if (twice != single || single < 3u) std::fprintf(stderr, " window stores: written twice %zu, written once %zu\n", twice, single); + return twice == single && single >= 3u; + }); + if (passed) std::puts("Vector window shape and SSA control tests passed"); return passed ? 0 : 1; } diff --git a/core/libs/prx/libSceAgcDriver/tests/VertexInputValidation.cpp b/core/libs/prx/libSceAgcDriver/tests/VertexInputValidation.cpp new file mode 100644 index 0000000000..bd6a291b22 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/VertexInputValidation.cpp @@ -0,0 +1,150 @@ +#include "Translation/ShaderInputInfoBuilder.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace ShaderRecompiler; + +namespace { + +struct BufferInput { + std::uint64_t address; + std::uint32_t stride; + std::uint32_t records; + std::uint32_t fetchIndex; +}; + +ShaderVertexStageInfo vertexInputs(const std::vector& buffers, bool embedded = false) { + ShaderVertexStageInfo vertex{}; + for (const auto& buffer : buffers) { + const auto index = vertex.resourcesNum++; + vertex.resources[index].fields = {static_cast(buffer.address), static_cast(buffer.address >> 32u) | buffer.stride << 16u, buffer.records, 0u}; + vertex.resourcesDst[index] = {static_cast(index * 4u), 4, static_cast(index), buffer.fetchIndex}; + } + vertex.fetchEmbedded = embedded; + return vertex; +} + +template +std::string errorOf(TRun&& run) { + try { + run(); + } catch (const std::exception& error) { + return error.what(); + } + return {}; +} + +} + +int main() { + std::vector> vertexCases{std::nullopt}; + auto overfull = vertexInputs({{0x10000u, 16u, 100u, 0u}}); + overfull.resourcesNum = ShaderVertexStageInfo::MaxResources + 1u; + vertexCases.push_back(overfull); + vertexCases.push_back(vertexInputs({{0x10000u, 16u, 100u, 0u}, {0x10004u, 16u, 99u, 0u}})); + vertexCases.push_back(vertexInputs({{0x10000u, 16u, 100u, 0u}, {0x10004u, 16u, 99u, 0u}}, true)); + vertexCases.push_back(vertexInputs({{0x10000u, 16u, 100u, 0u}, {0x10004u, 16u, 100u, 0u}, {0x20000u, 8u, 50u, 0u}, {0x10008u, 16u, 100u, 1u}})); + std::vector attributes; + std::vector separate; + for (std::uint32_t index = 0; index < ShaderVertexStageInfo::MaxResources; ++index) { + attributes.push_back({0x30000u + index * 4u, 128u, 10u, 0u}); + separate.push_back({0x40000u + index * 0x1000u, 4u + index * 4u, 10u, 0u}); + } + vertexCases.push_back(vertexInputs(attributes)); + vertexCases.push_back(vertexInputs(separate)); + + const MeshConfiguration mesh{3u, 64u, 64u, 64u, 64u, 64u, 0u, 0u}; + auto noThreads = mesh; + noThreads.threadsPerGroup = 0u; + auto badProvoking = mesh; + badProvoking.provokingVertex = 3u; + std::vector> meshCases{std::nullopt, mesh, noThreads, badProvoking}; + struct MeshExpectation { + std::uint32_t outputPrimitiveSize; + std::uint32_t provokingVertex; + bool accepted; + }; + constexpr std::array meshExpectations{MeshExpectation{0u, 0u, false}, MeshExpectation{1u, 0u, false}, MeshExpectation{2u, 0u, true}, MeshExpectation{2u, 1u, true}, MeshExpectation{2u, 2u, false}, MeshExpectation{3u, 2u, true}, MeshExpectation{3u, 3u, false}, MeshExpectation{4u, 0u, false}, MeshExpectation{4u, 3u, false}}; + const auto meshWith = [&](const MeshExpectation& expectation) { + auto varied = mesh; + varied.outputPrimitiveSize = expectation.outputPrimitiveSize; + varied.provokingVertex = expectation.provokingVertex; + return varied; + }; + for (const auto& expectation : meshExpectations) meshCases.push_back(meshWith(expectation)); + + const TessellationConfiguration tessellation{3u, 3u, 1u, 2u, 2u, 16u}; + std::vector> tessellationCases{std::nullopt, tessellation}; + const auto vary = [&](auto change) { + auto varied = tessellation; + change(varied); + tessellationCases.push_back(varied); + }; + vary([](TessellationConfiguration& config) { config.inputControlPoints = 0u; }); + vary([](TessellationConfiguration& config) { config.outputControlPoints = 33u; }); + vary([](TessellationConfiguration& config) { config.domain = 3u; }); + vary([](TessellationConfiguration& config) { config.partitioning = 1u; }); + vary([](TessellationConfiguration& config) { config.outputTopology = 1u; }); + vary([](TessellationConfiguration& config) { config.patchesPerGroup = 0u; }); + vary([](TessellationConfiguration& config) { config.patchesPerGroup = 257u; }); + + constexpr std::array stages{ShaderStageKind::Vertex, ShaderStageKind::Local, ShaderStageKind::TessellationControl, ShaderStageKind::TessellationEvaluation, ShaderStageKind::Mesh}; + std::size_t mismatches = 0; + std::size_t accepted = 0; + std::set errors; + for (const auto stage : stages) { + for (std::size_t vertexCase = 0; vertexCase < vertexCases.size(); ++vertexCase) { + for (std::size_t meshCase = 0; meshCase < meshCases.size(); ++meshCase) { + for (std::size_t tessellationCase = 0; tessellationCase < tessellationCases.size(); ++tessellationCase) { + GuestContext context{}; + context.waveSize = 64u; + context.vertex = vertexCases[vertexCase]; + const auto* meshConfig = meshCases[meshCase] ? &*meshCases[meshCase] : nullptr; + const auto* tessellationConfig = tessellationCases[tessellationCase] ? &*tessellationCases[tessellationCase] : nullptr; + const auto built = errorOf([&] { static_cast(BuildShaderStageInputInfo(stage, context, 64u, meshConfig, tessellationConfig)); }); + const auto validated = errorOf([&] { ValidateVertexStageInputs(stage, context, meshConfig, tessellationConfig); }); + if (built != validated) { + ++mismatches; + std::fprintf(stderr, "stage %d, vertex case %zu, mesh case %zu, tessellation case %zu: the build says \"%s\", the validation \"%s\"\n", static_cast(stage), vertexCase, meshCase, tessellationCase, built.c_str(), validated.c_str()); + } + if (built.empty()) ++accepted; + else errors.insert(built); + } + } + } + } + const std::array expected{"GuestContext.vertex is not set", "invalid vertex resource count", "disagree on record count", "tessellation configuration is missing", "invalid tessellation control-point counts", "unsupported tessellation configuration", "invalid tessellation patches per group", "has no mesh configuration", "invalid mesh configuration"}; + for (const auto* message : expected) { + const bool seen = std::any_of(errors.begin(), errors.end(), [&](const std::string& error) { return error.find(message) != std::string::npos; }); + if (!seen) { + ++mismatches; + std::fprintf(stderr, "no case raised \"%s\"\n", message); + } + } + GuestContext meshContext{}; + meshContext.waveSize = 64u; + meshContext.vertex = vertexInputs({{0x10000u, 16u, 100u, 0u}}); + for (const auto& expectation : meshExpectations) { + const auto varied = meshWith(expectation); + const bool built = errorOf([&] { static_cast(BuildShaderStageInputInfo(ShaderStageKind::Mesh, meshContext, 64u, &varied, nullptr)); }).empty(); + const bool validated = errorOf([&] { ValidateVertexStageInputs(ShaderStageKind::Mesh, meshContext, &varied, nullptr); }).empty(); + if (built != expectation.accepted || validated != expectation.accepted) { + ++mismatches; + std::fprintf(stderr, "mesh output primitive size %u, provoking vertex %u: expected %s, the build %s, the validation %s\n", expectation.outputPrimitiveSize, expectation.provokingVertex, expectation.accepted ? "accepted" : "rejected", built ? "accepts" : "rejects", validated ? "accepts" : "rejects"); + } + } + if (accepted == 0) { + ++mismatches; + std::fprintf(stderr, "no case was accepted\n"); + } + if (mismatches != 0) return 1; + std::printf("vertex input validation matches the input info build in %zu cases (%zu accepted)\n", stages.size() * vertexCases.size() * meshCases.size() * tessellationCases.size(), accepted); + return 0; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/WaveControlDecode.cpp b/core/libs/prx/libSceAgcDriver/tests/WaveControlDecode.cpp index 707159cd03..c202166efb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WaveControlDecode.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WaveControlDecode.cpp @@ -105,6 +105,8 @@ void verifyTranslation() { const auto orderedAdd = withStore({0xd8fe0000u, 0x05000001u}); require(recompile(orderedAdd).empty(), "ds_ordered_count adding to a GDS counter is refused: " + recompile(orderedAdd)); require(recompile(orderedAdd, 32u).empty(), "ds_ordered_count of a wave64 on 32-lane subgroups is refused: " + recompile(orderedAdd, 32u)); + const auto orderedSwap = withStore({0xd8fe1000u, 0x05000001u}); + require(recompile(orderedSwap).empty(), "ds_ordered_count swapping a GDS counter is refused: " + recompile(orderedSwap)); const std::vector, std::string_view>> refused{ {"s_sethalt 1", withStore({0xbf8d0001u}), "s_sethalt 1 at pc 0 halts the wave"}, @@ -117,7 +119,6 @@ void verifyTranslation() { {"ds_gws_sema_p", withStore({0xd8720000u, 0x00000000u}), "ds_gws_sema_p at pc 0: global wave sync"}, {"ds_gws_sema_release_all", withStore({0xd8620000u, 0x00000000u}), "ds_gws_sema_release_all at pc 0: global wave sync"}, {"ds_gws_barrier", withStore({0xd8760000u, 0x00000003u}), "ds_gws_barrier at pc 0: global wave sync"}, - {"ds_ordered_count swap", withStore({0xd8fe1000u, 0x05000001u}), "ds_ordered_count at pc 0: ordered swap is not modeled"}, {"ds_ordered_count of two dwords", withStore({0xd8fe4000u, 0x05000001u}), "ds_ordered_count at pc 0: counting more than one dword is not modeled"}, }; for (const auto& [name, code, message] : refused) { diff --git a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp index 179d8f18a9..700455ddd0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp @@ -7,6 +7,7 @@ #define NOMINMAX #endif #include +#include #else #include #include @@ -19,6 +20,7 @@ #include #include #include +#include #include #include @@ -29,12 +31,79 @@ using namespace AgcDriver::GuestMemory; constexpr std::size_t Block = 65536; #ifdef _WIN32 constexpr wchar_t HostHoleMode[] = L"--host-hole"; +constexpr wchar_t ResidentRemapMode[] = L"--resident-direct-remap"; #endif void Require(bool condition, const std::string& message) { if (!condition) throw std::runtime_error(message); } +#ifdef _WIN32 +bool CheckMetadataCommit() { + std::uintptr_t arena = 0; + std::size_t bytes = 0; + GuestArena::GuestArenaRange_nid_postfix(&arena, &bytes); + if (bytes == 0 || !GuestArena::GuestArenaWriteWatched_nid_postfix()) return false; + PROCESS_MEMORY_COUNTERS_EX before{}; + PROCESS_MEMORY_COUNTERS_EX after{}; + Require(K32GetProcessMemoryInfo(GetCurrentProcess(), reinterpret_cast(&before), sizeof(before)), "cannot measure private commit before tracking"); + Require(WriteWatched(), "the arena is not write watched"); + Require(K32GetProcessMemoryInfo(GetCurrentProcess(), reinterpret_cast(&after), sizeof(after)), "cannot measure private commit after tracking"); + const auto committed = after.PrivateUsage > before.PrivateUsage ? after.PrivateUsage - before.PrivateUsage : 0; + std::cout << "write-tracker initialization: " << committed / 1024 << " KiB private commit for " << (bytes >> 30u) << " GiB guest arena\n"; + Require(committed < (64u << 20u), "write tracking committed dense generation tables for untouched guest ranges"); + const auto generation = TrackerGeneration(); + for (std::uint64_t offset = 0; offset + Block <= bytes; offset += std::uint64_t{1} << 32u) { + Require(NewestStamp(arena + offset, Block) == 0 && UnchangedSince(arena + offset, Block, generation), "an untouched leaf has a write stamp"); + } + PROCESS_MEMORY_COUNTERS_EX queried{}; + Require(K32GetProcessMemoryInfo(GetCurrentProcess(), reinterpret_cast(&queried), sizeof(queried)), "cannot measure private commit after empty-range queries"); + Require(queried.PrivateUsage < after.PrivateUsage + (4u << 20u), "queries allocated untouched tracking leaves"); + return true; +} + +void CheckSparseLeafBoundaries() { + constexpr std::uintptr_t LeafBytes = std::uintptr_t{1} << 32; + std::uintptr_t arena = 0; + std::size_t arenaBytes = 0; + GuestArena::GuestArenaRange_nid_postfix(&arena, &arenaBytes); + const auto boundary = arena + (arenaBytes / (2 * LeafBytes)) * LeafBytes; + constexpr std::size_t bytes = 4 * Block; + void* memory = GuestArena::GuestArenaAllocateAtOrAbove_nid_postfix(boundary - Block, bytes, Block); + struct Release { + void* memory; + ~Release() { + GuestArena::GuestArenaReset_nid_postfix(memory, bytes); + GuestArena::GuestArenaRelease_nid_postfix(memory, bytes); + } + } release{memory}; + const auto base = reinterpret_cast(memory); + Require(base == boundary - Block, "the sparse tracking fixture did not straddle its 4 GiB leaf boundary"); + GuestArena::GuestArenaCommit_nid_postfix(memory, bytes, PAGE_READWRITE, bytes); + std::memset(memory, 0x11, bytes); + BumpCollectEpoch(); + const auto collected = CollectWrites(base, bytes); + Require(collected != 0, "the cross-leaf range was not collected"); + MarkWritten(boundary, 4); + Require(UnchangedSince(base, Block, collected) && !UnchangedSince(base, bytes, collected), "a driver store across a leaf boundary did not stamp exactly its block"); + Require(UnchangedSinceCollected(boundary, Block, collected), "the cross-leaf driver store became a CPU store"); + static_cast(memory)[0] = 0x22; + CollectWritesUncached(base, bytes); + Require(!UnchangedSinceCollected(base, Block, collected), "a CPU store before the leaf boundary was missed"); + Require(UnchangedSinceCollected(boundary, 3 * Block, collected), "the CPU store stamped the other leaf"); + Unwatch(base, bytes); + const auto hint = UntrackedHint(base, bytes); + Require(hint != 0, "the cross-leaf untracked range has no hint"); + static_cast(memory)[0] = 0x33; + static_cast(memory)[3 * Block] = 0x44; + BumpCollectEpoch(); + const auto changed = UntrackedHint(base, bytes); + Require(changed > hint, "cross-leaf CPU stores did not change their untracked hint"); + Require(UntrackedHint(base, Block) == changed && UntrackedHint(base + 3 * Block, Block) == changed, "a cross-leaf hint store was missed"); + Require(UntrackedHint(boundary, 2 * Block) == hint, "hint stores changed untouched blocks in the other leaf"); +} +#endif + void* AllocateWatched(std::size_t bytes, bool crossLeaf = false) { #ifdef _WIN32 static_cast(crossLeaf); @@ -580,6 +649,82 @@ void CheckRewatch() { GuestArena::GuestArenaRelease_nid_postfix(memory, 4 * Block); } +void CheckResidentDirectMappingReuse() { + constexpr std::size_t bytes = 5 * Block; + auto* memory = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, Block)); + Require(memory != nullptr, "cannot reserve the resident remap fixture"); + struct Cleanup { + std::uint8_t* memory; + std::array sections{}; + ~Cleanup() { + GuestArena::GuestArenaReset_nid_postfix(memory, bytes); + GuestArena::GuestArenaRelease_nid_postfix(memory, bytes); + for (const auto section : sections) { + if (section == nullptr) continue; + GuestArena::GuestArenaForgetSection_nid_postfix(section); + CloseHandle(section); + } + } + } cleanup{memory}; + GuestArena::GuestArenaCommit_nid_postfix(memory, 3 * Block, PAGE_READWRITE, 3 * Block); + GuestArena::GuestArenaCommit_nid_postfix(memory + 3 * Block, 2 * Block, PAGE_READWRITE, 2 * Block); + Require(Watched(reinterpret_cast(memory), bytes), "the resident remap fixture is not watched"); + for (auto& section : cleanup.sections) { + section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE, 0, 3 * Block, nullptr); + Require(section != nullptr, "cannot create the resident remap backing"); + } + const auto base = reinterpret_cast(memory); + const auto nativeType = [&](std::size_t offset) { + MEMORY_BASIC_INFORMATION info{}; + Require(VirtualQuery(memory + offset, &info, sizeof(info)) == sizeof(info), "cannot query the resident remap backing"); + return info.Type; + }; + for (std::size_t use = 0; use < 2; ++use) { + BumpCollectEpoch(); + const auto before = CollectWrites(base, 3 * Block); + Require(before != 0, "the backing before a resident remap was not collected"); + Unwatch(base + Block, Block); + Require(!Watched(base + Block, Block), "the resident remap exclusion was not established"); + const auto outside = CollectWrites(base + 3 * Block, 2 * Block); + const auto mapping = GuestArena::GuestArenaCommitGeneration_nid_postfix(); + GuestArena::GuestArenaMapDirect_nid_postfix(memory, 3 * Block, cleanup.sections[use], 0, PAGE_READWRITE); + Require(nativeType(0) == MEM_PRIVATE && nativeType(2 * Block) == MEM_PRIVATE, "the resident remap did not create private memory"); + Require(Watched(base, 3 * Block), "a resident direct remap retained an old import exclusion"); + Require(GuestArena::GuestArenaCommitGeneration_nid_postfix() > mapping, "a resident direct remap reused the mapping generation"); + Require(!UnchangedSince(base, Block, before) && !UnchangedSince(base + 2 * Block, Block, before), "a resident direct remap reused old neighbour stamps"); + Require(UnchangedSince(base + 3 * Block, 2 * Block, outside), "a resident direct remap invalidated memory outside its range"); + const auto after = CollectWritesUncached(base, 3 * Block); + Require(after > before && UnchangedSince(base, 3 * Block, after), "the resident remap could not establish a current generation"); + memory[Block + 9] = static_cast(0x51 + use); + CollectWritesUncached(base, 3 * Block); + Require(!UnchangedSince(base + Block, Block, after), "a CPU store after a resident direct remap was missed"); + std::cout << "resident direct remap " << use << ": private, watched, new mapping generation, CPU store collected\n"; + } + Unwatch(base, 3 * Block); + GuestArena::GuestArenaMap_nid_postfix(memory, 3 * Block, cleanup.sections[0], 0, PAGE_READWRITE); + Require(nativeType(0) == MEM_MAPPED, "the shared remap did not create mapped memory"); + for (std::size_t block = 0; block < 3; ++block) Require(!Watched(base + block * Block, Block), "an ordinary shared remap restored a private import exclusion"); + GuestArena::GuestArenaMapDirect_nid_postfix(memory + 3 * Block, 2 * Block, cleanup.sections[2], 0, PAGE_READWRITE); + GuestArena::GuestArenaMapDirect_nid_postfix(memory, 3 * Block, cleanup.sections[2], 0, PAGE_READWRITE); + Require(nativeType(0) == MEM_MAPPED && nativeType(Block) == MEM_MAPPED && nativeType(2 * Block) == MEM_PRIVATE, "the mixed direct remap did not establish shared and private spans"); + Require(!Watched(base, Block) && !Watched(base + Block, Block) && Watched(base + 2 * Block, Block), "the mixed direct remap restored an exclusion outside its new private span"); + std::cout << "resident direct remap controls: shared exclusions retained, only the new private span restored\n"; + Unwatch(base, 3 * Block); + bool refused = false; + try { + GuestArena::GuestArenaMapDirect_nid_postfix(memory, 3 * Block, cleanup.sections[1], 0, 0); + } catch (const std::system_error& error) { + Require(error.code().value() == ERROR_INVALID_PARAMETER, "the failed resident remap reported an unexpected error"); + refused = true; + } + std::uintptr_t createdBegin = 0; + std::uintptr_t createdEnd = 0; + Require(refused && GuestArena::GuestArenaResidentSpan_nid_postfix(base, 1, &createdBegin, &createdEnd), "the failing protection change did not leave a new private span"); + Require(createdBegin == base && createdEnd <= base + 3 * Block && Watched(base, createdEnd - base), "a failed resident remap did not report its completed private span"); + for (auto block = createdEnd; block < base + 3 * Block; block += Block) Require(!Watched(block, Block), "a failed resident remap restored an uncreated span"); + std::cout << "resident direct remap failure: completed private span watched, original mapping error retained\n"; +} + void CheckUntrackedHint() { constexpr std::size_t bytes = 4 * Block; auto* memory = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, Block)); @@ -731,6 +876,39 @@ void CheckHostHole() { }) && imported, "an import into an arena host hole did not run"); Require(!Watched(hole, bytes), "an import made an arena host hole watched"); } + +std::uint64_t failedCollectBegin = 0; +std::uint64_t failedCollectEnd = 0; +std::uint64_t unwatchHookBegin = 0; +std::uint64_t unwatchHookEnd = 0; + +void RecordUnwatch(std::uint64_t address, std::size_t bytes) { + unwatchHookBegin = address; + unwatchHookEnd = address + bytes; +} + +bool FailFreshCollect(std::uintptr_t address, std::size_t bytes, void (*)(void*, std::uintptr_t, std::uintptr_t), void*) { + failedCollectBegin = address; + failedCollectEnd = address + bytes; + return false; +} + +void CheckFailedCollectUnwatch() { + void* memory = AllocateWatched(3 * Block); + const auto base = reinterpret_cast(memory); + std::memset(memory, 0x11, 3 * Block); + Require(Watched(base + Block, Block), "the probe block was not watched"); + SetFreshCollectHook(&FailFreshCollect); + SetUnwatchHook(&RecordUnwatch); + const auto stored = MarkWritten(base + Block + 0x8000, 4); + SetFreshCollectHook(nullptr); + SetUnwatchHook(nullptr); + Require(stored == 0, "a failed fresh collect produced trusted stamps"); + Require(!Watched(base + Block, Block), "a failed collect left its block watched"); + Require(Watched(base, Block) && Watched(base + 2 * Block, Block), "a failed collect unwatched unrelated blocks"); + Require(failedCollectBegin == base + Block && failedCollectEnd == base + 2 * Block, "the failed fresh collect did not cover its block"); + Require(unwatchHookBegin <= base + Block && unwatchHookEnd >= base + 2 * Block, "a failed collect did not run the unwatch hook over its block"); +} #endif void CheckVersionRanges() { @@ -944,6 +1122,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; @@ -1186,6 +1395,11 @@ void CheckAdoptLatestEpoch() { int main() { try { +#ifdef _WIN32 + if (std::wstring_view(GetCommandLineW()).ends_with(L"--metadata-commit")) { + return CheckMetadataCommit() ? 0 : 77; + } +#endif if (!WriteWatched()) { std::cout << "no write watching: skipped\n"; return 77; @@ -1195,6 +1409,10 @@ int main() { CheckHostHole(); return 0; } + if (std::wstring_view(GetCommandLineW()).ends_with(ResidentRemapMode)) { + CheckResidentDirectMappingReuse(); + return 0; + } #endif CheckSharedBlock(); CheckStoredAllSince(); @@ -1209,17 +1427,20 @@ int main() { #endif CheckCoarseRuns(); #ifdef _WIN32 + CheckSparseLeafBoundaries(); CheckDeferredSharedImports(); CheckPrivateMappingReuse(); CheckRewatch(); CheckUntrackedHint(); CheckHostHoleInChild(); + CheckFailedCollectUnwatch(); #endif CheckVersionRanges(); CheckCollectBoundaries(); CheckImportWindow(); CheckImportWindowZeroPages(); CheckImportWindowOtherThread(); + CheckImportWindowOverUnwatchedBlock(); CheckStoreAcrossImports(); CheckUnwatchedImport(); CheckCommittedRanges(); diff --git a/core/libs/prx/libSceAgcDriver/tests/WriteWatchCoverage.cpp b/core/libs/prx/libSceAgcDriver/tests/WriteWatchCoverage.cpp index 806c3334e7..62e56cfe52 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WriteWatchCoverage.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WriteWatchCoverage.cpp @@ -41,7 +41,8 @@ int main() { require(coverage.Covers(base + block, block), "clipping an import excluded the rest of the arena"); coverage.Initialize(base, 4 * block + 1); coverage.Exclude(base, 4 * block + 1); - require(!coverage.Restore(base + 1, block - 1) && !coverage.Covers(base, block), "a partial replacement restored an excluded block"); + require(coverage.Restore(base + 1, block - 1) && coverage.Covers(base + 4096, block - 4096), "a partial replacement did not restore its complete pages"); + require(!coverage.Covers(base, 4096) && !coverage.Covers(base, block), "a partial replacement restored an incompletely remapped page"); require(coverage.Restore(base + block, block) && coverage.Covers(base + block, block), "fresh private backing stayed excluded"); require(!coverage.Restore(base + block, block), "restoring an already watched block reports a change"); require(!coverage.Covers(base, block) && !coverage.Covers(base + 2 * block, block), "restoring one block restored its neighbours"); @@ -60,6 +61,44 @@ int main() { coverage.Exclude(base, block, 7); require(!coverage.Restore(base, block, 6) && !coverage.Covers(base, block), "a delayed mapping notification erased a repeated import"); require(coverage.Restore(base, block, 8), "a fresh replacement did not restore the latest import exclusion"); + coverage.Initialize(base, 2 * block); + coverage.Exclude(base, block, 10); + require(coverage.Restore(base + 16384, 16384, 11), "a fresh partial-block private mapping stayed excluded"); + require(coverage.Covers(base + 16384, 16384), "the restored private pages are not watched"); + require(!coverage.Covers(base, 16384) && !coverage.Covers(base + 32768, 32768), "restoring private pages watched an imported neighbour"); + require(!coverage.Covers(base + 16383, 2) && !coverage.Covers(base + 32767, 2), "a boundary query included excluded pages"); + require(!coverage.Restore(base + 32768, 4096, 10), "a stale partial mapping restored an exclusion"); + coverage.Exclude(base + 4096, 1, 12); + require(!coverage.Covers(base + 16384, 16384), "a repeated import kept partial coverage from an older mapping"); + require(!coverage.Restore(base + 16384, 16384, 11), "a delayed partial mapping restored a newer import"); + require(coverage.Restore(base + 16384, 16384, 13), "fresh partial coverage was not restored after re-import"); + constexpr std::size_t page = 4096; + for (std::size_t first = 0; first < block / page; ++first) { + for (std::size_t count = 1; count <= block / page - first; ++count) { + coverage.Initialize(base, 3 * block); + coverage.Exclude(base + block, block, 20); + require(coverage.Restore(base + block + first * page, count * page, 21), "a complete-page replacement was not restored"); + for (std::size_t index = 0; index < 3 * block / page; ++index) { + const bool expected = index < block / page || index >= 2 * block / page || (index >= block / page + first && index < block / page + first + count); + require(coverage.Covers(base + index * page, page) == expected, "partial restoration changed an unrelated page"); + if (index != 0) { + const bool previous = index - 1 < block / page || index - 1 >= 2 * block / page || (index - 1 >= block / page + first && index - 1 < block / page + first + count); + require(coverage.Covers(base + index * page - 1, 2) == (previous && expected), "a page boundary proof ignored an excluded page"); + } + } + require(coverage.Covers(base + block, block) == (count == block / page), "partial page coverage proved a whole block"); + require(!coverage.Restore(base + block + first * page, count * page, 21), "restoring watched pages again reports a change"); + } + } + coverage.Initialize(base, 2 * block + 17); + coverage.Exclude(base + 2 * block, 17, 30); + require(!coverage.Restore(base + 2 * block, 16, 31), "an incomplete terminal page was restored"); + require(coverage.Restore(base + 2 * block, 17, 31) && coverage.Covers(base + 2 * block, 17), "a complete terminal arena page stayed excluded"); + coverage.Initialize(base, 2 * block); + coverage.Exclude(base, block, 40); + coverage.Exclude(base + block, block, 43); + require(coverage.Restore(base + block - page, 2 * page, 42), "a newer boundary page was not restored"); + require(coverage.Covers(base + block - page, page) && !coverage.Covers(base + block, page), "a stale boundary mapping erased a newer neighbouring exclusion"); std::cout << "write watch coverage passed\n"; return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp index 09713f0f50..5c662f4b2d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" #include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" @@ -38,6 +39,16 @@ #include #include +#ifndef _WIN32 +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} +#endif + namespace { using AgcDriver::Graphics::ColorTargetLayout; @@ -618,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}) { @@ -676,6 +733,187 @@ void DccFastClearTests(AgcDriver::VulkanDevice& device, std::uint32_t selectedSa } +void DccRefillTests(AgcDriver::VulkanDevice& device, std::uint32_t selectedSamples) { + 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}; +#ifndef _WIN32 + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + const auto decided = AgcDriver::Graphics::PrepareImportWatch(context); + struct Restore { + const AgcDriver::Graphics::Context& context; + AgcDriver::Graphics::ImportWatch decided; + ~Restore() { AgcDriver::Graphics::SetImportWatch(context, decided); } + } restore{context, decided}; +#endif + const auto fill = [](const Block& block, std::size_t bytes, std::uint32_t pattern) { + for (std::size_t offset = 0; offset + sizeof(pattern) <= bytes; offset += sizeof(pattern)) std::memcpy(block.data + offset, &pattern, sizeof(pattern)); + AgcDriver::Graphics::NoteColorMetadataFill(block.Address(), bytes, pattern); + }; + const Color written{0.25f, 0.25f, 0.25f, 0.25f}; + for (const auto samples : {2u, 4u, 8u}) { + if (selectedSamples != 0 && samples != selectedSamples) continue; + for (const auto mode : {0u, 1u, 2u, 3u, 4u, 5u, 6u}) { + for (const auto cmasked : {false, true}) { + if (mode == 4 && !cmasked) continue; +#ifdef _WIN32 + if (mode == 6) continue; +#else + AgcDriver::Graphics::SetImportWatch(context, mode == 5 ? AgcDriver::Graphics::ImportWatch::Watch : mode == 6 ? AgcDriver::Graphics::ImportWatch::Unwatch : decided); +#endif + 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; + Block* cmask = nullptr; + if (cmasked) { + blocks.push_back(std::make_unique(BlockBytes)); + cmask = blocks.back().get(); + std::memset(cmask->data, 0xcc, BlockBytes); + target.cmaskAddress = cmask->Address(); + target.cmaskBytes = AgcDriver::Graphics::CmaskBytes(Width, Height); + target.cmaskFastClear = true; + device.ColorMetadataPass({AgcDriver::Graphics::ColorMetadataPass::Mode::FmaskDecompress, {target}}); + } + Draw(device, target, false, Float32Export, Program(written, Float32Export, 1u), false, true); + const auto dccBytes = AgcDriver::Graphics::MultisampledDccBytes(target); + std::string_view expected; + bool rewatchable = false; + if (mode == 0) { + fill(metadata, dccBytes, 0xc0c0c0c0u); + if (cmask != nullptr) fill(*cmask, target.cmaskBytes, 0xccccccccu); + } else if (mode == 1) { + fill(metadata, dccBytes, 0xc0c0c0c0u); + } else if (mode >= 5) { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + const auto record = [&](const Block& block, std::size_t bytes, std::uint32_t value) { + const std::array pattern{value, value, value, value}; + Require(device.FillBuffer(block.Address(), bytes, pattern), "the metadata fill was not recorded on the GPU"); + AgcDriver::Graphics::NoteColorMetadataFill(block.Address(), bytes, value); + }; + record(metadata, dccBytes, 0xc0c0c0c0u); + if (cmask != nullptr) record(*cmask, target.cmaskBytes, 0xccccccccu); + if (!AgcDriver::GuestMemory::Watched(metadata.Address(), dccBytes) || (cmask != nullptr && !AgcDriver::GuestMemory::Watched(cmask->Address(), target.cmaskBytes))) expected = "requires watched guest memory"; +#ifdef _WIN32 + rewatchable = !expected.empty(); +#else + Require(mode == 6 || expected.empty(), "a GPU fill of multisampled DCC metadata left it unwatched although host imports stay watched"); +#endif + } else if (mode == 2) { + fill(metadata, dccBytes / 2u, 0xc0c0c0c0u); + expected = "writes to a resident multisampled DCC"; + } else if (mode == 3) { + fill(metadata, dccBytes, 0xffffffffu); + expected = "not a supported uniform fast clear"; + } else { + fill(*cmask, target.cmaskBytes, 0xccccccccu); + expected = "writes to a resident multisampled DCC"; + } + bool drawn = false; + if (!expected.empty()) { + bool refused = false; + try { + Draw(device, target, false, Float32Export, Program(written, Float32Export, 1u), false, true); + drawn = true; + } catch (const std::runtime_error& error) { + refused = std::string_view(error.what()).find(expected) != std::string_view::npos; + } + Require(refused || (drawn && rewatchable), "a write to resident multisampled DCC metadata that is not a full fast-clear fill was not refused"); + if (refused) { + programCount = 0; + continue; + } + } + if (!drawn) Draw(device, target, 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 = target; + 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 expectedValue = x < ScissorWidth ? 1.0f + (written[channel] - 1.0f) / static_cast(samples) : 1.0f; + const auto actual = FromHalf(stored[layout.Offset(x, y) / 2u + channel]); + Require(actual == expectedValue, "a full DCC fast-clear fill of a resident multisampled target did not clear it to the new color before the next draw"); + } + } + } + programCount = 0; + } + } + } +} + +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(); @@ -685,6 +923,8 @@ int main(int argc, char** argv) { Require(argc == 2 || samples != 0, "unknown DCC test sample count"); 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/CachedImageMultisampleGuard.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/CachedImageMultisampleGuard.cpp new file mode 100644 index 0000000000..c60b857816 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/CachedImageMultisampleGuard.cpp @@ -0,0 +1,248 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" +#include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "VulkanTestDevice.hpp" +#include "ImageTableAbi.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using namespace AgcDriver::Graphics; + +constexpr std::uint32_t Side = 64; +constexpr std::size_t BlockBytes = 65536; +constexpr std::size_t AllocationBytes = 2 * BlockBytes; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + data = GuestArena::GuestArenaAllocate_nid_postfix(AllocationBytes, BlockBytes); + if (data != nullptr) GuestArena::GuestArenaCommit_nid_postfix(data, AllocationBytes, PAGE_READWRITE, AllocationBytes); +#else + void* mapped = mmap(nullptr, AllocationBytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapped != MAP_FAILED) { + const auto begin = reinterpret_cast(mapped); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(mapped, aligned - begin); + if (aligned + AllocationBytes != begin + AllocationBytes + BlockBytes) munmap(reinterpret_cast(aligned + AllocationBytes), begin + BlockBytes - aligned); + data = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, AllocationBytes); + } +#endif + Require(data != nullptr, "cannot allocate tracked image memory"); + std::memset(data, 0, AllocationBytes); + GuestAllocations::Mutation().Add(data, AllocationBytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(data); +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(data, AllocationBytes); + GuestArena::GuestArenaRelease_nid_postfix(data, AllocationBytes); +#else + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, AllocationBytes); + munmap(data, AllocationBytes); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + std::uint64_t Address() const { return reinterpret_cast(data); } + +private: + void* data = nullptr; +}; + +template +void RequireColorGuard(TCall&& call, const char* failure) { + bool guarded = false; + try { + call(); + } catch (const std::runtime_error& error) { + guarded = std::string_view(error.what()).find("multisampled color target") != std::string_view::npos; + if (!guarded) throw; + } + Require(guarded, failure); +} + +void CheckCachedImage(AgcDriver::VulkanDevice& device, bool sampled, bool draw) { + std::printf("checking %s %s image reuse\n", draw ? "draw" : "compute", sampled ? "sampled" : "storage"); + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + GuestBlock block; + struct Cleanup { + VkDevice device; + ~Cleanup() { + ClearMultisampleTargets(device); + ClearCachedTextures(device); + } + } cleanup{context.device}; + const auto address = block.Address(); + ShaderRecompiler::DescriptorBinding binding{}; + binding.kind = sampled ? ShaderRecompiler::DescriptorKind::SampledImage : ShaderRecompiler::DescriptorKind::StorageImage; + binding.role = ShaderRecompiler::DescriptorRole::GuestImages; + binding.binding = (sampled ? ShaderRecompiler::RuntimeAbi::FirstImageBinding : ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding) + PlaceholderDimensionsPerClass + 2u; + binding.count = 1; + binding.imageShape = ShaderRecompiler::DescriptorImageShape::Image2D; + binding.imageWritten = {false}; + binding.imageSamplers = {0u}; + binding.guestDescriptor = {static_cast(address >> 8u), static_cast((address >> 40u) & 0xffu) | (20u << 20u) | (((Side - 1u) & 3u) << 30u), ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), 0xfacu | (9u << 28u), 0u, 0u, 0u, 0u}; + ShaderRecompiler::RecompileResult vertex; + ShaderRecompiler::RecompileResult program; + program.bindings.push_back(binding); + const CompiledShader shader{draw ? ShaderRecompiler::ShaderStage::Fragment : ShaderRecompiler::ShaderStage::Compute, &program, 0}; + const std::array stages{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, shader}}; + const auto build = [&] { + if (draw) return std::make_unique(context, stages, ColorTarget{}, 0, 0, 0); + return std::make_unique(context, shader); + }; + auto resources = build(); + Require(resources->Reusable(), "the ordinary image resources are not reusable"); + const auto revalidate = [&](ShaderResources::ProofReport* report = nullptr) { + return draw ? resources->Revalidate(stages, report) : resources->Revalidate(shader, report); + }; + ShaderResources::ProofReport proof; + Require(revalidate(&proof), "the unchanged image resources failed their initial proof"); + if (std::getenv("APS5_NO_FAST_REVALIDATE") == nullptr) { + if (AgcDriver::GuestMemory::Watched(address, Side * Side * 4u)) Require(proof.path == ShaderResources::ProofPath::Fast, "the fixture did not exercise the fast image proof"); + else std::puts("image memory is compared, not watched: fast image proof not tested; checking the image guards through the full proof"); + } + ColorTarget target{}; + target.address = address + BlockBytes; + target.surfaceAddress = target.address; + target.extent = {Side, Side}; + target.surfaceExtent = target.extent; + target.format = VK_FORMAT_R8G8B8A8_UNORM; + target.tileMode = ColorTileMode::RenderTarget; + target.elementBytes = 4; + target.bytes = Side * Side * 4u; + target.samples = 4; + Require(MultisampleTargetView(context, target) != VK_NULL_HANDLE, "the disjoint multisample target has no view"); + device.SubmitRecorded(true); + device.WaitIdle(); + Require(revalidate(), "a disjoint multisample target prevented image reuse"); + target.address = address; + target.surfaceAddress = address; + Require(MultisampleTargetView(context, target) != VK_NULL_HANDLE, "the overlapping multisample target has no view"); + device.SubmitRecorded(true); + device.WaitIdle(); + RequireColorGuard([&] { revalidate(); }, "a cached image access accepted memory owned by a multisampled color target"); + RequireColorGuard([&] { build(); }, "a fresh image access accepted memory owned by a multisampled color target"); + ClearMultisampleTargets(context.device); + Require(revalidate(), "retiring the multisample targets did not restore image reuse"); +} + +void CheckMultisampleTable(AgcDriver::VulkanDevice& device) { + namespace Abi = ShaderRecompiler::ImageTableAbi; + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + if (context.imageTableCapacity == 0) { + std::puts("skipped, native multisample table check needs image table support"); + return; + } + GuestBlock block; + struct Cleanup { + VkDevice device; + ~Cleanup() { ClearMultisampleTargets(device); } + } cleanup{context.device}; + ColorTarget target{}; + target.address = block.Address(); + target.surfaceAddress = target.address; + target.extent = {Side, Side}; + target.surfaceExtent = target.extent; + target.format = VK_FORMAT_R8G8B8A8_UNORM; + target.tileMode = ColorTileMode::RenderTarget; + target.elementBytes = 4; + target.bytes = Side * Side * 4u; + target.samples = 4; + Require(MultisampleTargetView(context, target) != VK_NULL_HANDLE, "the native multisample table target has no view"); + device.SubmitRecorded(true); + device.WaitIdle(); + ShaderRecompiler::RecompileResult program; + ShaderRecompiler::DescriptorBinding map{}; + map.kind = ShaderRecompiler::DescriptorKind::StorageBuffer; + map.role = ShaderRecompiler::DescriptorRole::ImageTableMap; + map.binding = static_cast(ShaderRecompiler::RuntimeAbi::Binding::ImageTableMap); + map.count = 1; + map.guestDescriptor.resize(Abi::HeaderWords + 1u); + map.guestDescriptor[Abi::HeaderVersion] = Abi::Version; + map.guestDescriptor[Abi::HeaderElementCount] = 1u; + map.guestDescriptor[Abi::HeaderElementFaults] = Abi::HeaderWords; + map.guestDescriptor[Abi::HeaderSamplerFaults] = Abi::HeaderWords + 1u; + program.bindings.push_back(map); + ShaderRecompiler::DescriptorBinding image{}; + image.kind = ShaderRecompiler::DescriptorKind::SampledImage; + image.role = ShaderRecompiler::DescriptorRole::ImageTable; + image.binding = ShaderRecompiler::RuntimeAbi::ImageTableBinding; + image.count = 1; + image.imageShapes = {ShaderRecompiler::DescriptorImageShape::Image2DMultisample}; + image.imageDepthCompare = {false}; + image.imageWritten = {false}; + image.imageSamplers = {0u}; + const auto address = block.Address(); + image.guestDescriptor = {static_cast(address >> 8u), static_cast((address >> 40u) & 0xffu) | (56u << 20u) | (((Side - 1u) & 3u) << 30u), ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), 0xfacu | (2u << 16u) | (0x1bu << 20u) | (14u << 28u), 0u, 2u << 4u, 0u, 0u}; + program.bindings.push_back(image); + const CompiledShader shader{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + ShaderResources resources(context, shader); + Require(resources.SampledTextures().size() == 1u, "the native multisample table entry was rejected during construction"); + Require(resources.Reusable(), "the native multisample table did not reach resource validation"); + static_cast(resources.Revalidate(shader)); + device.SubmitRecorded(true); + device.WaitIdle(); + std::puts("native multisample table validation did not reject its color target"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(*device); + if (!AgcDriver::GuestMemory::WriteWatched() || (context.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_4_BIT) == 0) { + Require(std::getenv("ANYPS5_REQUIRE_VULKAN") == nullptr, "cached image guard tests require write tracking and 4x color targets"); + std::puts("skipped, cached image guard tests require write tracking and 4x color targets"); + return VulkanTestSkipped; + } + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + for (const bool draw : {false, true}) { + for (const bool sampled : {false, true}) CheckCachedImage(*device, sampled, draw); + } + CheckMultisampleTable(*device); + std::puts("cached image multisample guard tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/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/ComputeTableCalls.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ComputeTableCalls.cpp index 6b3bb066da..d9a6a8aa1d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ComputeTableCalls.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ComputeTableCalls.cpp @@ -28,6 +28,10 @@ alignas(256) constexpr std::array Caller{ 0x7e020280u, 0xf4080400u, 0xfa000000u, 0x8f148402u, 0xbf8cc07fu, 0xf4280208u, 0x28000000u, 0xbf8cc07fu, 0xbe8e0308u, 0xbe8f0309u, 0xbf068103u, 0xbe8e210eu, 0x34040082u, 0xe0701000u, 0x80010102u, 0xbf810000u, 0xbf9f0000u, 0x30306c73u, 0x00000040u, 0x00000008u}; +alignas(256) constexpr std::array HoistedCaller{ + 0x7e020280u, 0xf4080400u, 0xfa000000u, 0xbf8cc07fu, 0xbf068203u, 0xbf850001u, 0x7e020280u, 0x8f148402u, + 0xf4280208u, 0x28000000u, 0xbf8cc07fu, 0xbe8e0308u, 0xbe8f0309u, 0xbf068103u, 0xbe8e210eu, 0x34040082u, + 0xe0701000u, 0x80010102u, 0xbf810000u, 0xbf9f0000u, 0x30306c73u, 0x00000040u, 0x00000008u}; alignas(256) constexpr std::array SelectSixteen{0x85159091u, 0x7e020215u, 0xbe80200eu}; alignas(256) constexpr std::array SelectThirtyTwo{0x8515a0a1u, 0x7e020215u, 0xbe80200eu}; alignas(16) std::array Table{}; @@ -49,7 +53,7 @@ std::array UserData(std::uint32_t index, bool scc) { static_cast(output), static_cast((output >> 32u) & 0xffffu), static_cast(sizeof(Output)), 0x31016facu}; } -std::vector Commands(const std::array& data, bool wave32) { +std::vector Commands(const std::array& data, bool wave32, const std::uint32_t* caller) { std::vector words; const auto registers = [&](std::uint32_t first, std::span values) { words.push_back(0xc0007600u | (static_cast(values.size()) << 16u)); @@ -58,7 +62,7 @@ std::vector Commands(const std::array& data, bo }; const std::array shape{Threads, 1, 1}; registers(0x207, shape); - const auto address = reinterpret_cast(Caller.data()); + const auto address = reinterpret_cast(caller); const std::array program{static_cast(address >> 8u), static_cast(address >> 40u)}; registers(0x20c, program); const std::array resource{static_cast(data.size()) << 1u}; @@ -105,9 +109,9 @@ private: int saved = -1; }; -std::string Dispatch(std::uint32_t index, bool scc, bool wave32) { +std::string Dispatch(std::uint32_t index, bool scc, bool wave32, const std::uint32_t* caller = Caller.data()) { Output.fill(Untouched); - const auto words = Commands(UserData(index, scc), wave32); + const auto words = Commands(UserData(index, scc), wave32, caller); Packet packet{const_cast(words.data()), static_cast(words.size()), 0, {}}; StderrCapture log; std::string failure; @@ -121,8 +125,8 @@ std::string Dispatch(std::uint32_t index, bool scc, bool wave32) { return log.Finish() + failure; } -void Check(std::uint32_t index, bool scc, bool wave32, std::uint32_t expected, const std::string& what) { - const auto log = Dispatch(index, scc, wave32); +void Check(std::uint32_t index, bool scc, bool wave32, std::uint32_t expected, const std::string& what, const std::uint32_t* caller = Caller.data()) { + const auto log = Dispatch(index, scc, wave32, caller); Require(log.find("no linked callee") == std::string::npos, what + ": a linked callee was reported missing: " + log); for (std::uint32_t lane = 0; lane < Threads; ++lane) { Require(Output[lane] == expected, what + (wave32 ? " (wave32)" : " (wave64)") + ": lane " + std::to_string(lane) + " is " + std::to_string(Output[lane]) + ", expected " + std::to_string(expected)); @@ -143,6 +147,8 @@ int main() { Check(0u, true, wave32, 17u, "the first record's callee with SCC 1"); Check(1u, false, wave32, 32u, "the second record's callee with SCC 0"); Check(1u, true, wave32, 33u, "the second record's callee with SCC 1"); + Check(0u, false, wave32, 16u, "the first record's callee through a descriptor loaded before a branch", HoistedCaller.data()); + Check(1u, true, wave32, 33u, "the second record's callee through a descriptor loaded before a branch", HoistedCaller.data()); const auto log = Dispatch(2u, false, wave32); Require(log.find("called address 0x0 at program counter") != std::string::npos && log.find("which no linked callee of that call matches") != std::string::npos, std::string("a record past the table did not report an unlinked call") + (wave32 ? " (wave32)" : " (wave64)") + ": " + log); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearHarness.hpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearHarness.hpp index a5c5f41b01..e1d74d89e9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearHarness.hpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearHarness.hpp @@ -41,6 +41,7 @@ struct Surface { VkFormat format; std::uint32_t slice = 0; std::uint8_t clearStencil = 0; + std::uint32_t swizzle = 0; }; struct DrawOptions { @@ -71,11 +72,11 @@ struct Shaders { std::uint32_t vertexPush = 0; }; -inline Shaders Compile(AgcDriver::VulkanDevice& device) { +inline Shaders Compile(AgcDriver::VulkanDevice& device, const void* vertices = Triangles().data()) { const auto target = device.Target(); Shaders shaders; std::vector vertexUserData(4, 0u); - const auto vertexBuffer = BufferDescriptor(Triangles().data(), 16u, static_cast(Triangles().size())); + const auto vertexBuffer = BufferDescriptor(vertices, 16u, static_cast(Triangles().size())); std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; ShaderRecompiler::RecompileRequest vertex{ @@ -121,6 +122,7 @@ inline std::uint32_t Draw(AgcDriver::VulkanDevice& device, const Shaders& compil state.hasColorTarget = true; state.depth = AgcDriver::Graphics::DepthTarget{surface.depth, surface.stencil, extent, surface.format, 1.0f, surface.clearStencil, surface.htile, surface.htileStencil}; state.depth->firstSlice = surface.slice; + state.depth->depthSwizzle = surface.swizzle; state.depthTest = options.depthTest; state.depthWrite = options.depthWrite; state.depthCompare = options.depthCompare; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneBufferMemory.cpp new file mode 100644 index 0000000000..fcaadb3722 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneBufferMemory.cpp @@ -0,0 +1,238 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "DepthFastClearHarness.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using DepthFastClearHarness::Compile; +using DepthFastClearHarness::Covered; +using DepthFastClearHarness::Draw; +using DepthFastClearHarness::Height; +using DepthFastClearHarness::HtileBytes; +using DepthFastClearHarness::Require; +using DepthFastClearHarness::Surface; +using DepthFastClearHarness::Width; + +constexpr std::size_t Block = 65536; +alignas(256) std::array Htile{}; +alignas(256) std::array ViewedHtile{}; +alignas(256) constexpr std::array LoadCode{ + 0x7e02020du, 0x7e04020cu, 0xf0001108u, 0x00010300u, 0xbf8c3f70u, 0x34080082u, 0xe0701000u, 0x80000304u, 0xbf810000u, 0xbf810000u, 0xbf810000u, +}; +constexpr std::uint32_t LoadThreads = 32; +constexpr std::size_t ColorBytes = static_cast(Width) * Height * 4u; + +void* AllocateWatched(std::size_t bytes) { +#ifdef _WIN32 + void* block = GuestArena::GuestArenaAllocate_nid_postfix(bytes, Block); + GuestArena::GuestArenaCommit_nid_postfix(block, bytes, PAGE_READWRITE, bytes); +#else + void* raw = mmap(nullptr, bytes + Block, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot map the depth plane block"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + Block - 1) & ~static_cast(Block - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + Block) munmap(reinterpret_cast(aligned + bytes), begin + Block - aligned); + void* block = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, bytes); +#endif + GuestAllocations::Mutation().Add(block, bytes, true, true, true); + return block; +} + +std::array LoadRow(AgcDriver::VulkanDevice& device, std::uint64_t address, std::uint32_t width, std::uint32_t height, std::uint32_t swizzle) { + alignas(256) static std::array output{}; + std::vector userData(16, 0u); + const auto out = reinterpret_cast(output.data()); + const std::array buffer{static_cast(out), static_cast((out >> 32u) & 0xffffu), static_cast(sizeof(output)), 0x31016facu}; + const std::array texture{static_cast(address >> 8u), static_cast((address >> 40u) & 0xffu) | (22u << 20u) | (((width - 1u) & 3u) << 30u), ((width - 1u) >> 2u) | ((height - 1u) << 14u), 0xfacu | (swizzle << 20u) | (9u << 28u), 0u, 0u, 0u, 0u}; + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + const std::span code(LoadCode); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{LoadThreads, 1, 1}, 0, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderRecompiler::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; + output.fill(0xdeadbeefu); + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + return output; +} + +bool RowHolds(const std::array& row, float depth) { + return std::all_of(row.begin(), row.end(), [&](std::uint32_t bits) { return bits == std::bit_cast(depth); }); +} + +std::size_t Holding(std::span plane, float depth) { + return static_cast(std::count(plane.begin(), plane.end(), depth)); +} + +void DrawBlendKeepingMemory(AgcDriver::VulkanDevice& device, const DepthFastClearHarness::Shaders& compiled, std::uint64_t address) { + const std::array shaders{{ + {ShaderRecompiler::ShaderStage::Vertex, &compiled.vertex, 0}, + {ShaderRecompiler::ShaderStage::Fragment, &compiled.pixel, PixelPushOffset(compiled.vertexPush, device.Target())} + }}; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt}; + state.color = {address, {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, ColorBytes, 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0.0f, 1.0f}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend = {VK_TRUE, VK_BLEND_FACTOR_ZERO, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_ADD, VK_BLEND_FACTOR_ZERO, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_ADD, 15}; + state.blends = {state.blend}; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(DepthFastClearHarness::Triangles().size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto planeBytes = static_cast(AgcDriver::Graphics::DepthSliceBytes({Width, Height}, 4u)); + Require(planeBytes <= Block, "the depth plane does not fit one block"); + auto* block = static_cast(AllocateWatched(Block)); + const std::span plane(block, planeBytes / sizeof(float)); + const auto address = reinterpret_cast(block); + const auto shaders = Compile(*device); + const auto reader = Compile(*device, block); + const Surface depth{address, 0, 0, false, VK_FORMAT_D32_SFLOAT, 0, 0, AgcDriver::Graphics::XorSwizzleMode(AgcDriver::Graphics::TextureTileMode::kZ64KBX)}; + + Require(Draw(*device, shaders, depth, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, "the draw writing depth 0.5 did not cover the target"); + Draw(*device, reader, depth, {.depthTest = false}); + Require(Holding(plane, 0.5f) == static_cast(Width) * Height, "a vertex fetch from the depth plane did not see the depth 0.5 a draw wrote into it"); + + if (!AgcDriver::GuestMemory::WriteWatched() || !AgcDriver::GuestMemory::Watched(address, Block)) { + std::puts("guest memory is not write-watched here: a buffer write into the depth plane is not exercised"); + std::puts("depth plane buffer memory tests passed"); + return 0; + } + std::fill(plane.begin(), plane.end(), 0.75f); + AgcDriver::GuestMemory::BumpCollectEpoch(); + Require(Draw(*device, shaders, depth, {}) == Covered, "a draw at depth 0.5 failed LESS against the 0.75 written into the depth plane's memory"); + Require(Draw(*device, shaders, depth, {.depthWrite = true}) == Covered, "a draw at depth 0.5 writing depth failed LESS against 0.75"); + Draw(*device, reader, depth, {.depthTest = false}); + Require(Holding(plane, 0.5f) == static_cast(Width) * Height, "a vertex fetch from the depth plane did not see the depth 0.5 written after the plane was reloaded"); + + auto* fresh = static_cast(AllocateWatched(Block)); + const std::span freshPlane(fresh, planeBytes / sizeof(float)); + const auto freshReader = Compile(*device, fresh); + const Surface unstored{reinterpret_cast(fresh), 0, 0, false, VK_FORMAT_D32_SFLOAT, 0, 0, depth.swizzle}; + Require(Draw(*device, shaders, unstored, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, "the draw writing depth 0.5 into a new plane did not cover the target"); + std::fill(freshPlane.begin(), freshPlane.end(), 0.75f); + AgcDriver::GuestMemory::BumpCollectEpoch(); + Draw(*device, freshReader, depth, {.depthTest = false}); + Require(Holding(freshPlane, 0.75f) == freshPlane.size(), "the depth 0.5 a draw wrote before a guest write into the plane was stored over that write"); + Require(Draw(*device, shaders, unstored, {}) == Covered, "a draw at depth 0.5 failed LESS against the 0.75 written over depth the surface had not stored"); + std::fill(freshPlane.begin(), freshPlane.end(), 0.25f); + AgcDriver::GuestMemory::BumpCollectEpoch(); + Require(Draw(*device, shaders, unstored, {}) == 0u, "a draw at depth 0.5 passed LESS against the 0.25 written into the plane before it"); + Require(Draw(*device, shaders, unstored, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, "the draw writing depth 0.5 over the reloaded plane did not cover the target"); + Draw(*device, freshReader, depth, {.depthTest = false}); + Require(Holding(freshPlane, 0.5f) == static_cast(Width) * Height, "the depth 0.5 a draw wrote after a guest write into the plane did not reach memory"); + + Htile.fill(0xffffffffu); + auto* cleared = static_cast(AllocateWatched(Block)); + const std::span clearedPlane(cleared, planeBytes / sizeof(float)); + const auto clearedReader = Compile(*device, cleared); + const Surface compressed{reinterpret_cast(cleared), 0, reinterpret_cast(Htile.data()), false, VK_FORMAT_D32_SFLOAT, 0, 0, depth.swizzle}; + Require(Draw(*device, shaders, compressed, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, "the draw writing depth 0.5 into a plane with HTILE did not cover the target"); + Draw(*device, clearedReader, depth, {.depthTest = false}); + Require(Holding(clearedPlane, 0.5f) == static_cast(Width) * Height, "a vertex fetch from a plane with HTILE did not see the depth 0.5 a draw wrote into it"); + std::fill(clearedPlane.begin(), clearedPlane.end(), 0.25f); + AgcDriver::GuestMemory::BumpCollectEpoch(); + AgcDriver::Graphics::NoteDepthMetadataFill(compressed.htile, HtileBytes, 0u); + Require(Draw(*device, shaders, compressed, {}) == Covered, "a guest write into the depth plane won over a later HTILE fast clear of it"); + + auto* viewed = static_cast(AllocateWatched(Block)); + const std::span viewedBlock(viewed, Block / sizeof(float)); + std::fill(viewedBlock.begin(), viewedBlock.end(), 0.25f); + AgcDriver::GuestMemory::BumpCollectEpoch(); + ViewedHtile.fill(0xffffffffu); + const Surface sampled{reinterpret_cast(viewed), 0, reinterpret_cast(ViewedHtile.data()), false, VK_FORMAT_D32_SFLOAT, 0, 0, depth.swizzle}; + const auto viewedReader = Compile(*device, viewed); + constexpr std::size_t ViewOffset = 0x100; + const auto view = sampled.depth + ViewOffset; + const auto matchesMemory = [&](const std::array& row) { + Draw(*device, viewedReader, depth, {.depthTest = false}); + return std::memcmp(row.data(), reinterpret_cast(viewed) + ViewOffset, sizeof(row)) == 0; + }; + Require(Draw(*device, shaders, sampled, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, "the draw writing depth 0.5 into a viewed plane did not cover the target"); + Require(matchesMemory(LoadRow(*device, view, Width, Height / 2u, 0u)), "a linear texture at an offset into a depth plane did not read the depth drawn into it"); + AgcDriver::Graphics::NoteDepthMetadataFill(sampled.htile, HtileBytes, 0u); + Draw(*device, shaders, sampled, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_NEVER}); + Require(matchesMemory(LoadRow(*device, view, Width, Height / 2u, 0u)), "a cached linear texture at an offset into a depth plane did not read the depth cleared after it was cached"); + Require(Draw(*device, shaders, sampled, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, "the draw writing depth 0.5 over the cleared viewed plane did not cover the target"); + Require(matchesMemory(LoadRow(*device, view, Width, Height / 2u, 0u)), "a linear texture at an offset into a depth plane, viewed through its cached storage image, did not read the depth drawn after the image was made"); + + auto* blended = static_cast(AllocateWatched(Block)); + const std::span blendedBlock(blended, Block / sizeof(float)); + std::fill(blendedBlock.begin(), blendedBlock.end(), 0.25f); + AgcDriver::GuestMemory::BumpCollectEpoch(); + const Surface underBlend{reinterpret_cast(blended), 0, 0, false, VK_FORMAT_D32_SFLOAT, 0, 0, depth.swizzle}; + Require(Draw(*device, shaders, underBlend, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, "the draw writing depth 0.5 under a later blended color target did not cover the target"); + DrawBlendKeepingMemory(*device, shaders, underBlend.depth); + Draw(*device, Compile(*device, blended), depth, {.depthTest = false}); + Require(Holding(blendedBlock, 0.5f) == static_cast(Width) * Height, "a color target blending over a depth plane did not load the depth drawn into the plane"); + Htile.fill(0xffffffffu); + auto* reused = static_cast(AllocateWatched(Block)); + const std::span reusedPlane(reused, planeBytes / sizeof(float)); + const auto reusedReader = Compile(*device, reused); + const Surface recleared{reinterpret_cast(reused), 0, reinterpret_cast(Htile.data()), false, VK_FORMAT_D32_SFLOAT, 0, 0, depth.swizzle}; + Require(Draw(*device, shaders, recleared, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, "the draw writing depth 0.5 into a plane that is cleared each frame did not cover the target"); + Draw(*device, reusedReader, depth, {.depthTest = false}); + Require(Holding(reusedPlane, 0.5f) == static_cast(Width) * Height, "a vertex fetch did not see the depth 0.5 drawn into a plane that is cleared each frame"); + std::fill(reusedPlane.begin(), reusedPlane.end(), 0.25f); + AgcDriver::GuestMemory::BumpCollectEpoch(); + for (int frame = 0; frame < 2; ++frame) { + const float value = 0.75f - 0.125f * static_cast(frame); + AgcDriver::Graphics::NoteDepthMetadataFill(recleared.htile, HtileBytes, 0u); + Require(Draw(*device, shaders, recleared, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS, .minDepth = value, .maxDepth = value}) == Covered, "the draw after the HTILE clear did not cover the target"); + Draw(*device, reusedReader, depth, {.depthTest = false}); + Require(Holding(reusedPlane, value) == static_cast(Width) * Height, "after a guest write into a stored depth plane, the depth drawn behind an HTILE clear did not reach memory (frame " + std::to_string(frame) + ")"); + } + std::puts("depth plane buffer memory tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/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/DispatchDifferingValueSets.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DispatchDifferingValueSets.cpp new file mode 100644 index 0000000000..374159ffe1 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DispatchDifferingValueSets.cpp @@ -0,0 +1,141 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using AgcDriver::DriverDetail::Driver; + +constexpr std::array Code{0xf4080100u, 0xfa000000u, 0xbf8cc07fu, 0x7e020280u, 0xe0702000u, 0x80010100u, 0xbf810000u}; +constexpr std::size_t SrtOffset = 4096; +constexpr std::size_t OutputOffset = 8192; +constexpr std::size_t BlockBytes = 65536; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(BlockBytes, 65536)); + if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, BlockBytes, PAGE_READWRITE, BlockBytes); +#else + void* mapped = mmap(nullptr, BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapped != MAP_FAILED) { + block = static_cast(mapped); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, BlockBytes); + } +#endif + Require(block != nullptr, "dispatch differing value sets: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint64_t At(std::size_t offset) const { return static_cast(reinterpret_cast(block + offset)); } + std::uint8_t* Bytes(std::size_t offset) const { return block + offset; } + +private: + std::uint8_t* block = nullptr; +}; + +std::vector Pm4(std::uint32_t opcode, std::initializer_list body) { + std::vector words{0xc0000000u | (static_cast(body.size() - 1) << 16u) | (opcode << 8u)}; + words.insert(words.end(), body); + return words; +} + +void WriteSrt(const GuestBlock& block, std::uint32_t records) { + const auto address = block.At(OutputOffset); + const std::array words{static_cast(address), static_cast((address >> 32u) & 0xffffu) | (4u << 16u), records, 0x00014204u}; + std::memcpy(block.Bytes(SrtOffset), words.data(), sizeof(words)); +} + +void Dispatch(const GuestBlock& block) { + const auto kernel = block.At(0); + const auto srt = block.At(SrtOffset); + std::vector words; + for (const auto& packet : {Pm4(0x76, {0x20c, static_cast(kernel >> 8u)}), Pm4(0x76, {0x20d, static_cast(kernel >> 40u) & 0xffu}), Pm4(0x76, {0x207, 64}), Pm4(0x76, {0x208, 1}), Pm4(0x76, {0x209, 1}), Pm4(0x76, {0x213, 2u << 1u}), Pm4(0x76, {0x240, static_cast(srt)}), Pm4(0x76, {0x241, static_cast(srt >> 32u)}), Pm4(0x15, {1, 1, 1, 0x41})}) { + words.insert(words.end(), packet.begin(), packet.end()); + } + Packet dcb{words.data(), static_cast(words.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&dcb) == 0, "dispatch differing value sets: the dispatch submission failed"); + AgcDriverWaitIdle_nid_postfix(); +} + +struct Totals { + std::uint64_t classified = 0, matched = 0; + std::uint64_t lastClassified = 0, lastMatched = 0; + + void Add() { + const auto counters = Driver::Get().DispatchCacheCounters(); + classified += counters.differingClassified >= lastClassified ? counters.differingClassified - lastClassified : counters.differingClassified; + matched += counters.differingMatchedPrior >= lastMatched ? counters.differingMatchedPrior - lastMatched : counters.differingMatchedPrior; + lastClassified = counters.differingClassified; + lastMatched = counters.differingMatchedPrior; + } +}; + +void AlternatingValueSets(const GuestBlock& block) { + Totals totals; + totals.Add(); + for (std::uint32_t dispatch = 0; dispatch < 5; ++dispatch) { + WriteSrt(block, dispatch % 2u == 0u ? 64u : 65u); + Dispatch(block); + totals.Add(); + } + std::printf("alternating value sets: %llu differing dispatches classified, %llu matched one of the last 3 value sets\n", static_cast(totals.classified), static_cast(totals.matched)); + Require(totals.classified == 4, "the four dispatches after the first were not all classified as differing (" + std::to_string(totals.classified) + ")"); + Require(totals.matched == 3, "the three dispatches bringing a value set seen before did not all match it (" + std::to_string(totals.matched) + ")"); +} + +} + +int main() { + try { +#ifdef _WIN32 + _putenv_s("APS5_PROFILE_DRAW", "1"); + _putenv_s("APS5_DISPATCH_VARIANTS", "1"); +#else + setenv("APS5_PROFILE_DRAW", "1", 1); + setenv("APS5_DISPATCH_VARIANTS", "1", 1); +#endif + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + GuestBlock block; + std::memcpy(block.Bytes(0), Code.data(), sizeof(Code)); + AlternatingValueSets(block); + AgcDriverShutdown_nid_postfix(); + std::puts("dispatch differing value sets: the differing statistics recognize a value set they saw before"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { AgcDriverShutdown_nid_postfix(); } catch (...) {} + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DrawDataHitUserWords.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DrawDataHitUserWords.cpp new file mode 100644 index 0000000000..13d67731a9 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DrawDataHitUserWords.cpp @@ -0,0 +1,233 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; + +alignas(256) constexpr std::array VertexCode{0x7e000208u, 0x7e020280u, 0x7e040280u, 0x7e0602f2u, 0xf80008cfu, 0x03020100u, 0xbf810000u, 0xbf810000u}; +alignas(256) constexpr std::array PixelCode{0xf4080100u, 0xfa000000u, 0xbf8cc07fu, 0xe0300000u, 0x80010000u, 0xbf8c3f70u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u, 0xbf810000u}; +alignas(256) constexpr std::array TriangleVertexCode{ + 0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u, + 0x7e060280u, 0x7e0802f2u, 0xf80008cfu, 0x04030201u, 0xbf810000u, +}; +alignas(256) constexpr std::array ArithmeticPixelCode{0xf4000100u, 0xfa000000u, 0xbf8cc07fu, 0x7e000204u, 0x06000002u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u, 0xbf810000u}; +alignas(256) std::array PixelConstant{}; +alignas(256) std::array ColorMemory{}; + +constexpr std::size_t BlockBytes = 65536; + +struct Header { + Shader shader{}; + ShaderUserData userData{}; +}; + +alignas(8) Header VertexHeader{}; +alignas(8) Header PixelHeader{}; +alignas(8) Header ArithmeticPixelHeader{}; + +void Registered(Header& header, const std::uint32_t* code, std::size_t bytes, std::uint8_t type) { + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18u; + header.shader.header_size = sizeof(Header); + header.shader.shader_size = static_cast(bytes); + header.shader.code = code; + header.shader.type = type; + header.shader.user_data = &header.userData; + AgcDriverRegisterShader_nid_postfix(&header.shader); +} + +std::uint32_t* GuestBlock() { +#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 + Require(block != nullptr, "data-only hit: cannot allocate a guest block"); + std::memset(block, 0, BlockBytes); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + return block; +} + +void Append(std::vector& commands, std::uint32_t opcode, std::initializer_list payload) { + commands.push_back(0xc0000000u | (static_cast(payload.size() - 1) << 16u) | (opcode << 8u)); + commands.insert(commands.end(), payload); +} + +void PixelUserData(std::vector& commands, const std::uint32_t* pointer, std::uint32_t unread) { + const auto address = reinterpret_cast(pointer); + Append(commands, 0x76, {0x00c, static_cast(address)}); + Append(commands, 0x76, {0x00d, static_cast(address >> 32u)}); + Append(commands, 0x76, {0x00e, unread}); +} + +std::vector Setup(bool triangle = false) { + const auto color = reinterpret_cast(ColorMemory.data()); + const auto vertex = reinterpret_cast(triangle ? TriangleVertexCode.data() : VertexCode.data()); + const auto pixel = reinterpret_cast(triangle ? ArithmeticPixelCode.data() : PixelCode.data()); + std::vector> context{ + {0x2d5, 0x2000}, + {0x1b6, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, + {0x31b, 0}, {0x31d, 0}, {0x3b0, (63u << 14u) | 3u}, + {0x3b8, 0x9000000}, {0x1e0, 0}, + {0xc, 0}, {0xd, 0x40040}, + {0x81, 0x80000000}, {0x82, 0x40040}, + {0x90, 0x80000000}, {0x91, 0x40040}, + {0x94, 0x80000000}, {0x95, 0x40040}, + {0x318, static_cast(color >> 8u)}, {0x390, static_cast(color >> 40u)}, + {0x10f, std::bit_cast(32.0f)}, {0x110, std::bit_cast(32.0f)}, + {0x111, std::bit_cast(-2.0f)}, {0x112, std::bit_cast(2.0f)}, + {0x113, std::bit_cast(1.0f)}, {0x114, 0}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)}, + }; + const std::vector> shader{ + {0xc8, static_cast(vertex >> 8u)}, {0xc9, static_cast(vertex >> 40u)}, {0x8b, triangle ? 0u : 1u << 1u}, {0x8c, 0}, + {0x008, static_cast(pixel >> 8u)}, {0x009, static_cast(pixel >> 40u)}, {0x00b, 3u << 1u}, + }; + if (triangle) context.insert(context.end(), {{0x2fe, 0}, {0x302, 0}, {0x306, 0}, {0x30a, 0}}); + std::vector commands; + Append(commands, 0x79, {0x242, 4}); + for (const auto& [offset, value] : context) Append(commands, 0x69, {offset, value}); + for (const auto& [offset, value] : shader) Append(commands, 0x76, {offset, value}); + return commands; +} + +AgcDriver::DriverDetail::DrawEntryCounters Submit(std::vector commands) { + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "the draw submission failed"); + AgcDriverWaitIdle_nid_postfix(); + return AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); +} + +std::string Counts(const AgcDriver::DriverDetail::DrawEntryCounters& counters) { + return std::to_string(counters.lookups) + " lookups, " + std::to_string(counters.dataHits) + " data-only hits, stages " + std::to_string(counters.dataStagesReused) + " reused / " + std::to_string(counters.dataStagesCompiled) + " compiled / " + std::to_string(counters.dataVerified) + " verified"; +} + +void UnreadUserWordsKeepTheStage(bool verify) { + Registered(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2); + Registered(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1); + auto* first = GuestBlock(); + auto* second = GuestBlock(); + const auto describe = [](std::uint32_t* table, std::uint32_t* data, float value) { + const auto address = reinterpret_cast(data); + data[0] = std::bit_cast(value); + table[0] = static_cast(address); + table[1] = static_cast(address >> 32u) & 0xffffu; + table[2] = 16; + table[3] = 0x4bfacu; + }; + describe(first, first + 1024, 1.0f); + describe(second, second + 1024, 0.5f); + + auto commands = Setup(); + PixelUserData(commands, first, 0x11); + Append(commands, 0x2d, {3, 2}); + auto counters = Submit(std::move(commands)); + Require(counters.lookups == 1 && counters.dataHits == 0, "the first draw: " + Counts(counters)); + commands.clear(); + PixelUserData(commands, first, 0x11); + Append(commands, 0x2d, {3, 2}); + counters = Submit(std::move(commands)); + Require(counters.inserts != 0, "the repeated register key was not admitted: " + Counts(counters)); + + commands.clear(); + PixelUserData(commands, first, 0x22); + Append(commands, 0x2d, {3, 2}); + const auto before = counters; + counters = Submit(std::move(commands)); + const auto reused = counters.dataStagesReused - before.dataStagesReused; + const auto compiled = counters.dataStagesCompiled - before.dataStagesCompiled; + Require(counters.dataHits == before.dataHits + 1, "a draw that changed only a user word no stage reads was not a data-only hit: " + Counts(counters)); + if (verify) Require(compiled == 2 && counters.dataVerified - before.dataVerified == 2, "verify mode: a draw that changed only an unread user word did not build and compare both stages: " + Counts(counters)); + else Require(reused == 2 && compiled == 0, "a draw that changed only a user word no stage reads did not reuse both stages: " + Counts(counters)); + + commands.clear(); + PixelUserData(commands, second, 0x22); + Append(commands, 0x2d, {3, 2}); + const auto moved = counters; + counters = Submit(std::move(commands)); + Require(counters.dataHits == moved.dataHits + 1, "a draw that pointed the pixel stage at another buffer descriptor was not a data-only hit: " + Counts(counters)); + Require(counters.dataStagesCompiled - moved.dataStagesCompiled >= 1, "a draw that pointed the pixel stage at another buffer descriptor reused the pixel stage: " + Counts(counters)); + if (!verify) Require(counters.dataStagesReused - moved.dataStagesReused == 1, "a draw that changed only the pixel stage's descriptor pointer did not reuse the vertex stage: " + Counts(counters)); +} + +unsigned BrightestColorByte() { + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(ColorMemory.data()), ColorMemory.size()); + unsigned brightest = 0; + for (const auto value : ColorMemory) brightest = std::max(brightest, static_cast(std::to_integer(value))); + return brightest; +} + +void ReadUserWordsChangeTheOutput() { + Registered(VertexHeader, TriangleVertexCode.data(), sizeof(TriangleVertexCode), 2); + Registered(ArithmeticPixelHeader, ArithmeticPixelCode.data(), sizeof(ArithmeticPixelCode), 1); + PixelConstant[0] = 0.25f; + auto* table = reinterpret_cast(PixelConstant.data()); + const auto draw = [&](float word, bool setup) { + auto commands = setup ? Setup(true) : std::vector{}; + PixelUserData(commands, table, std::bit_cast(word)); + Append(commands, 0x2d, {3, 2}); + Submit(std::move(commands)); + return BrightestColorByte(); + }; + draw(0.25f, true); + const auto first = draw(0.25f, false); + const auto before = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); + const auto second = draw(0.5f, false); + const auto after = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); + const auto closeTo = [](unsigned value, unsigned expected) { return value + 2 >= expected && value <= expected + 2; }; + Require(closeTo(first, 128), "a pixel stage adding user word 2 (0.25) to a loaded 0.25 wrote " + std::to_string(first) + ", expected 128"); + Require(closeTo(second, 191), "a pixel stage adding user word 2 changed to 0.5 wrote " + std::to_string(second) + ", expected 191 (" + Counts(after) + ", before: " + Counts(before) + ")"); +} + +} + +int main() { + try { + bool libraries = false; + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + libraries = device->GraphicsPipelineLibraries(); + } + if (!libraries && std::getenv("APS5_NO_GPL") == nullptr) return VulkanTestSkipped; + const bool verify = std::getenv("APS5_VERIFY_DRAW_DATA_HITS") != nullptr; + UnreadUserWordsKeepTheStage(verify); + ReadUserWordsChangeTheOutput(); + AgcDriverShutdown_nid_postfix(); + std::cout << "Data-only hits " << (verify ? "verified" : "reused") << " the stages whose read user words did not change\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DrawOffsetIndices.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DrawOffsetIndices.cpp index 0fa8c4ebcd..9413952247 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DrawOffsetIndices.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DrawOffsetIndices.cpp @@ -69,6 +69,15 @@ alignas(256) constexpr std::array PostAddCode{ 0xbf810000, }; +alignas(256) constexpr auto NoCarryCode = [] { + auto code = PostAddCode; + code[2] = 0x4a0a0a10u; + code[3] = 0xbf800000u; + code[4] = 0x4a101011u; + code[5] = 0xbf800000u; + return code; +}(); + alignas(256) constexpr std::array CarryCode{ 0x7e020280, 0x7e040280, 0xd70f7d05, 0x00020a10, @@ -184,6 +193,88 @@ alignas(256) constexpr std::array OtherVgprCode{ 0xbf810000, }; +alignas(256) constexpr std::array SadCode{ + 0x7e020280, 0x7e040280, + 0xd55d0005, 0x04150010, + 0xd70f7d08, 0x00021011, + 0xf4080506, 0xfa000000, + 0xf4080606, 0xfa000010, + 0x02060b08, + 0xbf8cc07f, + 0xe0002000, 0x80050a03, + 0xe0002000, 0x80060b03, + 0xbf8c3f70, + 0x4a08170a, 0x34080884, + 0x7e180301, 0x7e1a0302, 0x7e1c0305, 0x7e1e0308, + 0xe0781000, 0x80020c04, + 0x7e200280, 0x7e2202f2, + 0xf80008cf, 0x11101010, + 0xbf810000, +}; + +alignas(256) constexpr std::array ExecAllCode{ + 0xbefe04c1, 0x7e040280, + 0xd70f7d05, 0x00020a10, + 0xd70f7d08, 0x00021011, + 0xf4080506, 0xfa000000, + 0xf4080606, 0xfa000010, + 0x02060b08, + 0xbf8cc07f, + 0xe0002000, 0x80050a03, + 0xe0002000, 0x80060b03, + 0xbf8c3f70, + 0x4a08170a, 0x34080884, + 0x7e180301, 0x7e1a0302, 0x7e1c0305, 0x7e1e0308, + 0xe0781000, 0x80020c04, + 0x7e200280, 0x7e2202f2, + 0xf80008cf, 0x11101010, + 0xbf810000, +}; + +alignas(256) constexpr std::array ReloadCode{ + 0x7e020280, 0x7e040280, + 0xd70f7d05, 0x00020a10, + 0xd70f7d08, 0x00021011, + 0xf4080506, 0xfa000000, + 0xf4080606, 0xfa000010, + 0x02060b08, + 0xbf8cc07f, + 0xe0002000, 0x80050a03, + 0xe0002000, 0x80060b03, + 0xbf8c3f70, + 0x4a08170a, 0x34080884, + 0x7e180301, 0x7e1a0302, 0x7e1c0305, 0x7e1e0308, + 0xe0781000, 0x80020c04, + 0x7e200280, 0x7e2202f2, + 0xf80008cf, 0x11101010, + 0xf4040407, 0xfa000000, + 0xbf8cc07f, + 0x80121110, + 0xbf810000, +}; + +alignas(256) constexpr std::array SkippedReloadCode{ + 0x7e020280, 0x7e040280, + 0xd70f7d05, 0x00020a10, + 0xd70f7d08, 0x00021011, + 0xf4080506, 0xfa000000, + 0xf4080606, 0xfa000010, + 0x02060b08, + 0xbf8cc07f, + 0xe0002000, 0x80050a03, + 0xe0002000, 0x80060b03, + 0xbf8c3f70, + 0x4a08170a, 0x34080884, + 0x7e180301, 0x7e1a0302, 0x7e1c0305, 0x7e1e0308, + 0xe0781000, 0x80020c04, + 0x7e200280, 0x7e2202f2, + 0xf80008cf, 0x11101010, + 0xbf840001, + 0xbe900380, + 0x80121110, + 0xbf810000, +}; + alignas(256) constexpr std::array PixelCode{0x7e0002f2u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u}; constexpr std::uint32_t Width = 64; @@ -363,7 +454,7 @@ std::string Analysis(Fixture& fixture) { bool vertex; bool instance; }; - const std::array cases{{ + const std::array cases{{ {"a program that copies v5 and v8 before its fetch adds", VertexCode, true, true}, {"a program whose fetch adds are the only readers of v5, v8, s16 and s17", PostAddCode, false, false}, {"a program that reads the instance add's carry", CarryCode, false, true}, @@ -372,6 +463,10 @@ std::string Analysis(Fixture& fixture) { {"a program that writes EXEC before its fetch adds", ExecCode, true, true}, {"a program that writes another add's carry to s17 before its fetch adds", CarryWriteCode, false, true}, {"a program whose s16 add writes v0, not v5", OtherVgprCode, true, false}, + {"a program whose vertex add is an unclamped v_sad_u32", SadCode, false, false}, + {"a program that sets every EXEC bit with s_mov_b64 before its fetch adds", ExecAllCode, false, false}, + {"a program that reloads s16 and s17 with s_load_dwordx2 from s[14:15] after its fetch adds and reads them", ReloadCode, false, false}, + {"a program that reads s16 and s17 after a branch that can skip the write of s16", SkippedReloadCode, true, true}, }}; const auto userData = fixture.UserData(0x51u, 0x62u); std::string failures; @@ -452,15 +547,16 @@ std::string Run(Fixture& fixture) { failures += fixture.Check(what + ", on the CPU path", autoIndices, arguments.firstVertexOrIndex, arguments.firstInstance, true); } if (fixture.device->DrawIndirectSupport().firstInstance) { - { - const std::string what = "indexed indirect record whose fetch adds are the only readers of v5/v8, user data s16 0x51, s17 0x62"; - const auto vertex = fixture.Vertex(PostAddCode, staleUserData); + for (const auto* code : {&PostAddCode, &NoCarryCode}) { + const std::string what = std::string(code == &NoCarryCode ? "no-carry " : "carry ") + "indexed indirect record whose fetch adds are the only readers of v5/v8, user data s16 0x51, s17 0x62"; + const auto vertex = fixture.Vertex(*code, staleUserData); AgcDriver::Pm4::DrawParameters draw{indexAddress, static_cast(Indices.size()), 2, 0, 0, true, 0, 0}; draw.indirect = indexedIndirect; failures += Classified(fixture, what, vertex, staleUserData, draw, std::nullopt, Rule::InPlace); fixture.Place(indexedRecord[3], bufferIndices, indexedRecord[4]); fixture.Draw(vertex, draw); failures += fixture.Check(what + ", drawn in place on the GPU", bufferIndices, indexedRecord[3], indexedRecord[4], false); + if (code == &NoCarryCode) std::cout << "no-carry indexed indirect GPU readback checked\n"; } { const std::string what = "indexed indirect record whose packet names no register for its offsets, user data s16 0x51, s17 0x62"; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DrawUnsupportedBufferType.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DrawUnsupportedBufferType.cpp index 2c4ef6537e..c0a50d0476 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DrawUnsupportedBufferType.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DrawUnsupportedBufferType.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -31,7 +32,7 @@ void Append(std::vector& commands, std::uint32_t opcode, std::ini commands.insert(commands.end(), payload); } -void Draw(const std::array& descriptor) { +void Draw(const std::array& descriptor, std::uint32_t* label = nullptr) { const auto vertex = reinterpret_cast(VertexCode.data()); const auto pixel = reinterpret_cast(PixelCode.data()); const std::vector> context{ @@ -68,6 +69,10 @@ void Draw(const std::array& descriptor) { Append(commands, 0x69, {0x318, static_cast(color >> 8u)}); Append(commands, 0x69, {0x390, static_cast(color >> 40u)}); Append(commands, 0x2d, {3, 2}); + if (label != nullptr) { + const auto address = reinterpret_cast(label); + Append(commands, 0x37, {0x00100200u, static_cast(address), static_cast(address >> 32u), 7u}); + } Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; Require(sceAgcDriverSubmitDcb(&packet) == 0, "unsupported buffer type draw submission failed"); AgcDriverWaitIdle_nid_postfix(); @@ -92,15 +97,29 @@ void NamesShaderAndDescriptor() { throw std::runtime_error("a pixel shader read a buffer through a descriptor of type 2"); } +void SkipsRefusedDraw() { + const auto address = reinterpret_cast(Source.data()); + const std::array descriptor{static_cast(address), static_cast((address >> 32u) & 0xffffu), sizeof(Source), 0xa1b00facu}; + alignas(8) static std::uint32_t label = 0; + for (int submission = 0; submission < 2; ++submission) { + label = 0; + Draw(descriptor, &label); + Require(label == 7u, "the packet after a skipped refused draw was not executed"); + } + for (const auto byte : Pixels) Require(byte == std::byte{0}, "a skipped refused draw wrote its color target"); } -int main() { +} + +int main(int argc, char** argv) { try { + Require(argc == 1 || (argc == 2 && std::string_view(argv[1]) == "--skip-refused"), "invalid test arguments"); { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; } - NamesShaderAndDescriptor(); + if (argc == 2) SkipsRefusedDraw(); + else NamesShaderAndDescriptor(); try { AgcDriverShutdown_nid_postfix(); } catch (const std::exception&) {} std::puts("draw unsupported buffer type tests passed"); return 0; 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/F16FmaInvalidNan.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F16FmaInvalidNan.cpp new file mode 100644 index 0000000000..da5cdec6ce --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F16FmaInvalidNan.cpp @@ -0,0 +1,141 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 10; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 2; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array FmaCode{ + 0x34020084u, 0x34040083u, 0xe03c1000u, 0x80000401u, 0xbf8c3f70u, 0x7e140280u, 0xd74b000au, 0x041a0b04u, + 0x7e160306u, 0x6c160b04u, 0xe0741000u, 0x80010a02u, 0xbf810000u, +}; +alignas(256) constexpr std::array MixCode{ + 0x34020084u, 0x34040083u, 0xe03c1000u, 0x80000401u, 0xbf8c3f70u, 0xcc20600au, 0x1c120b04u, 0xcc20600bu, + 0x0c120d04u, 0xe0741000u, 0x80010a02u, 0xbf810000u, +}; + +using Rows = std::array, Threads>; + +constexpr Rows FmaRows{{ + {0x7c00u, 0x0000u, 0x0000u}, + {0x0000u, 0xfc00u, 0x3c00u}, + {0x8000u, 0x7c00u, 0xfc00u}, + {0x8000u, 0xfc00u, 0x3c00u}, + {0x7c00u, 0xbc00u, 0x7c00u}, + {0x7c00u, 0x8000u, 0xfc00u}, + {0x7c00u, 0x3c00u, 0xfc00u}, + {0xfc00u, 0x3c00u, 0x7c00u}, + {0x7c00u, 0x0000u, 0x7e00u}, + {0x8000u, 0xfc00u, 0x7d00u}, +}}; +constexpr Rows MixRows{{ + {0x00007c00u, 0x00000000u, 0x00000000u}, + {0x3c00fc00u, 0x00000000u, 0x80000000u}, + {0x7e007c00u, 0x00008000u, 0x80000000u}, + {0x7d00fc00u, 0x00000000u, 0x00000000u}, + {0x7c007c00u, 0x0000bc00u, 0xbf800000u}, + {0xfc007c00u, 0x00003c00u, 0x3f800000u}, + {0x7c00fc00u, 0x00004000u, 0x40000000u}, + {0xfc00fc00u, 0x0000b800u, 0xbf000000u}, + {0x7e00fc00u, 0x00008000u, 0x00000000u}, + {0x7fff7c00u, 0x00000000u, 0x80000000u}, +}}; + +struct Mode { + const char* name; + std::optional value; +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +void Run(AgcDriver::VulkanDevice& device, std::span code, const Rows& rows, const Mode& mode) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + std::copy(rows[tid].begin(), rows[tid].end(), &Input[tid * Inputs]); + Input[tid * Inputs + 3u] = 0u; + } + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const 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; + request.context.floatMode = mode.value; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(const Mode& mode, const Rows& rows, const char* const (&names)[Results], std::uint32_t expected) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t form = 0; form < Results; ++form) { + const std::uint32_t actual = Output[tid * Results + form]; + Require(actual == expected, std::string(names[form]) + " " + mode.name + " of " + Hex(rows[tid][0]) + ", " + Hex(rows[tid][1]) + ", " + Hex(rows[tid][2]) + " is " + Hex(actual) + ", expected " + Hex(expected)); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const Mode modes[] = { + {"without a known float mode", std::nullopt}, + {"with denormals flushed, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0x00u, true, false, false}}, + {"with denormals flushed, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0x00u, true, true, false}}, + {"with denormals kept, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0xf0u, true, false, false}}, + {"with denormals kept, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}}, + }; + constexpr const char* FmaNames[Results] = {"v_fma_f16", "v_fmac_f16"}; + constexpr const char* MixNames[Results] = {"v_fma_mix_f32 of three f16 sources", "v_fma_mix_f32 of an f16, an f32 and an f16 source"}; + for (const Mode& mode : modes) { + Run(*device, FmaCode, FmaRows, mode); + Check(mode, FmaRows, FmaNames, 0xfe00u); + if (!mode.value) continue; + Run(*device, MixCode, MixRows, mode); + Check(mode, MixRows, MixNames, 0xffc00000u); + } + std::puts("f16 fma invalid operation NaN tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp index 6aa88b0a7d..19beb4db4c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp @@ -505,13 +505,15 @@ int main() { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; const auto capabilities = device->Target().supportedCapabilities; - const bool hardware = std::find(capabilities.begin(), capabilities.end(), static_cast(spv::CapabilityDenormFlushToZero)) != capabilities.end(); + const bool declared = std::find(capabilities.begin(), capabilities.end(), static_cast(spv::CapabilityDenormFlushToZero)) != capabilities.end(); + const bool native = !declared && device->Target().float32FlushToZero; + const bool hardware = declared || native; if (hardware) { bool softwareDeclares = true; bool hardwareDeclares = false; const auto software = CompiledWords(*device, false, softwareDeclares); const auto hardwareWords = CompiledWords(*device, true, hardwareDeclares); - Require(!softwareDeclares && hardwareDeclares, "f32 denormal flush: only the hardware-flush module declares DenormFlushToZero 32"); + Require(!softwareDeclares && hardwareDeclares == declared, declared ? "f32 denormal flush: only the hardware-flush module declares DenormFlushToZero 32" : "f32 denormal flush: a module flushed natively declares DenormFlushToZero 32 without the device capability"); Require(hardwareWords < software, "f32 denormal flush: the hardware-flush module is " + std::to_string(hardwareWords) + " words, the emulated one " + std::to_string(software)); } for (const bool flush : {false, true}) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/F64AddMulDenormalModes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F64AddMulDenormalModes.cpp new file mode 100644 index 0000000000..afacd844cb --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F64AddMulDenormalModes.cpp @@ -0,0 +1,224 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 16; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 4; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020084u, 0x34040084u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0xd564000au, 0x00020d04u, 0xd565000cu, + 0x00020d04u, 0xe0781000u, 0x80010a02u, 0xbf810000u, +}; + +constexpr std::uint64_t Pairs[Threads][2] = { + {0x0000000000000001ull, 0x0000000000000000ull}, + {0x8000000000000001ull, 0x0000000000000001ull}, + {0x000fffffffffffffull, 0x0000000000000001ull}, + {0x0010000000000000ull, 0x8000000000000001ull}, + {0x0010000000000001ull, 0x8010000000000000ull}, + {0x0000000000000001ull, 0x3ff0000000000000ull}, + {0x000fffffffffffffull, 0x3ff0000000000001ull}, + {0x0010000000000001ull, 0x3feffffffffffffeull}, + {0x2000000000000001ull, 0x1ffffffffffffffeull}, + {0x0010000000000000ull, 0x3fe0000000000000ull}, + {0xbff0000000000000ull, 0x800fffffffffffffull}, + {0x3ca0000000000000ull, 0x0020000000000000ull}, + {0x4330000000000000ull, 0x0000000000000001ull}, + {0x8000000000000000ull, 0x8000000000000000ull}, + {0x0000000000000000ull, 0x800fffffffffffffull}, + {0x3fe0000000000000ull, 0x0010000000000001ull}, +}; +constexpr std::uint64_t Expected[4][Threads][2] = { + { + {0x0000000000000000ull, 0x0000000000000000ull}, + {0x0000000000000000ull, 0x8000000000000000ull}, + {0x0000000000000000ull, 0x0000000000000000ull}, + {0x0010000000000000ull, 0x8000000000000000ull}, + {0x0000000000000000ull, 0x8000000000000000ull}, + {0x3ff0000000000000ull, 0x0000000000000000ull}, + {0x3ff0000000000001ull, 0x0000000000000000ull}, + {0x3feffffffffffffeull, 0x0010000000000000ull}, + {0x2010000000000000ull, 0x0010000000000000ull}, + {0x3fe0000000000000ull, 0x0000000000000000ull}, + {0xbff0000000000000ull, 0x0000000000000000ull}, + {0x3ca0000000000000ull, 0x0000000000000000ull}, + {0x4330000000000000ull, 0x0000000000000000ull}, + {0x8000000000000000ull, 0x0000000000000000ull}, + {0x0000000000000000ull, 0x8000000000000000ull}, + {0x3fe0000000000000ull, 0x0000000000000000ull}, + }, + { + {0x0000000000000000ull, 0x0000000000000000ull}, + {0x0000000000000000ull, 0x8000000000000000ull}, + {0x0010000000000000ull, 0x0000000000000000ull}, + {0x0000000000000000ull, 0x8000000000000000ull}, + {0x0000000000000000ull, 0x8000000000000000ull}, + {0x3ff0000000000000ull, 0x0000000000000000ull}, + {0x3ff0000000000001ull, 0x0010000000000000ull}, + {0x3feffffffffffffeull, 0x0010000000000000ull}, + {0x2010000000000000ull, 0x0010000000000000ull}, + {0x3fe0000000000000ull, 0x0000000000000000ull}, + {0xbff0000000000000ull, 0x0000000000000000ull}, + {0x3ca0000000000000ull, 0x0000000000000000ull}, + {0x4330000000000000ull, 0x0010000000000000ull}, + {0x8000000000000000ull, 0x0000000000000000ull}, + {0x8000000000000000ull, 0x8000000000000000ull}, + {0x3fe0000000000000ull, 0x0000000000000000ull}, + }, + { + {0x0000000000000000ull, 0x0000000000000000ull}, + {0x0000000000000000ull, 0x8000000000000000ull}, + {0x0000000000000000ull, 0x0000000000000000ull}, + {0x0010000000000000ull, 0x8000000000000000ull}, + {0x0000000000000001ull, 0x8000000000000000ull}, + {0x3ff0000000000000ull, 0x0000000000000000ull}, + {0x3ff0000000000001ull, 0x0000000000000000ull}, + {0x3feffffffffffffeull, 0x0010000000000000ull}, + {0x2010000000000000ull, 0x0010000000000000ull}, + {0x3fe0000000000000ull, 0x0008000000000000ull}, + {0xbff0000000000000ull, 0x0000000000000000ull}, + {0x3ca0000000000000ull, 0x0000000000000001ull}, + {0x4330000000000000ull, 0x0000000000000000ull}, + {0x8000000000000000ull, 0x0000000000000000ull}, + {0x0000000000000000ull, 0x8000000000000000ull}, + {0x3fe0000000000000ull, 0x0008000000000000ull}, + }, + { + {0x0000000000000001ull, 0x0000000000000000ull}, + {0x0000000000000000ull, 0x8000000000000000ull}, + {0x0010000000000000ull, 0x0000000000000000ull}, + {0x000fffffffffffffull, 0x8000000000000000ull}, + {0x0000000000000001ull, 0x8000000000000000ull}, + {0x3ff0000000000000ull, 0x0000000000000001ull}, + {0x3ff0000000000001ull, 0x0010000000000000ull}, + {0x3feffffffffffffeull, 0x0010000000000000ull}, + {0x2010000000000000ull, 0x0010000000000000ull}, + {0x3fe0000000000000ull, 0x0008000000000000ull}, + {0xbff0000000000000ull, 0x000fffffffffffffull}, + {0x3ca0000000000000ull, 0x0000000000000001ull}, + {0x4330000000000000ull, 0x0010000000000000ull}, + {0x8000000000000000ull, 0x0000000000000000ull}, + {0x800fffffffffffffull, 0x8000000000000000ull}, + {0x3fe0000000000000ull, 0x0008000000000000ull}, + }, +}; + +struct Mode { + const char* name; + std::optional value; + std::uint32_t field; +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint64_t value) { + char text[24]; + std::snprintf(text, sizeof(text), "0x%016llx", static_cast(value)); + return text; +} + +void Run(AgcDriver::VulkanDevice& device, const Mode& mode) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + Input[tid * Inputs] = static_cast(Pairs[tid][0]); + Input[tid * Inputs + 1u] = static_cast(Pairs[tid][0] >> 32u); + Input[tid * Inputs + 2u] = static_cast(Pairs[tid][1]); + Input[tid * Inputs + 3u] = static_cast(Pairs[tid][1] >> 32u); + } + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const 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; + request.context.floatMode = mode.value; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +std::uint32_t Check(const Mode& mode, bool keepsDenormals) { + constexpr const char* Names[2] = {"v_add_f64", "v_mul_f64"}; + const std::array deviceFlushes{mode.field & 2u, mode.field & 1u, 0u}; + std::uint32_t skipped = 0; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t form = 0; form < 2u; ++form) { + const std::uint64_t actual = Output[tid * Results + form * 2u] | (static_cast(Output[tid * Results + form * 2u + 1u]) << 32u); + const std::uint64_t expected = Expected[mode.field][tid][form]; + if (actual == expected) continue; + const std::string message = std::string(Names[form]) + " " + mode.name + " of " + Hex(Pairs[tid][0]) + " and " + Hex(Pairs[tid][1]) + " is " + Hex(actual) + ", expected " + Hex(expected); + const bool deviceDependent = std::any_of(deviceFlushes.begin(), deviceFlushes.end(), [&](std::uint32_t flushed) { return Expected[flushed][tid][form] != expected; }); + Require(deviceDependent && !keepsDenormals, message); + std::printf("skipped %s, the device flushes f64 denormals without DenormPreserve support\n", message.c_str()); + ++skipped; + } + } + return skipped; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!TargetHasCapability(device->Target(), spv::CapabilityFloat64)) { + std::puts("skipped, the device has no shaderFloat64"); + return VulkanTestSkipped; + } + const Mode modes[] = { + {"without a known float mode", std::nullopt, 3u}, + {"f64 denormal mode 0, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0x30u, true, true, false}, 0u}, + {"f64 denormal mode 0, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0x30u, true, false, false}, 0u}, + {"f64 denormal mode 1, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0x70u, true, true, false}, 1u}, + {"f64 denormal mode 1, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0x70u, true, false, false}, 1u}, + {"f64 denormal mode 2, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0xb0u, true, true, false}, 2u}, + {"f64 denormal mode 2, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0xb0u, true, false, false}, 2u}, + {"f64 denormal mode 3, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}, 3u}, + {"f64 denormal mode 3, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0xf0u, true, false, false}, 3u}, + }; + bool keepsDenormals = device->Target().float64DenormPreserve; + std::uint32_t skipped = 0; + for (const Mode& mode : modes) { + Run(*device, mode); + const std::uint32_t modeSkipped = Check(mode, keepsDenormals); + keepsDenormals = keepsDenormals || (!mode.value.has_value() && modeSkipped == 0u); + skipped += modeSkipped; + } + std::printf("f64 add and mul denormal mode tests passed, %u device-dependent results skipped\n", skipped); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/F64RoundingDenormalModes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F64RoundingDenormalModes.cpp new file mode 100644 index 0000000000..e64de0f152 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F64RoundingDenormalModes.cpp @@ -0,0 +1,169 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 16; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020084u, 0x34040086u, 0xe0341000u, 0x80000401u, 0xbf8c3f70u, 0x7e143504u, 0x7e183104u, 0x7e1c7d04u, + 0x7e207b04u, 0x7e247904u, 0xe0781000u, 0x80010a02u, 0xe0781010u, 0x80010e02u, 0xe0701020u, 0x80011202u, + 0xbf810000u, +}; + +constexpr std::array Values{ + 0x0000000000000000ull, 0x8000000000000000ull, 0x0000000000000001ull, 0x8000000000000001ull, 0x000fffffffffffffull, 0x800fffffffffffffull, + 0x0008000000000000ull, 0x8008000000000000ull, 0x0010000000000000ull, 0x8010000000000000ull, 0x3fe0000000000000ull, 0xbff8000000000000ull, + 0x4006000000000000ull, 0x3fefffffffffffffull, 0xbfefffffffffffffull, 0x7fefffffffffffffull, 0xffefffffffffffffull, 0x4330000000000001ull, +}; + +struct Mode { + const char* name; + std::optional value; +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint64_t value) { + char text[24]; + std::snprintf(text, sizeof(text), "0x%016llx", static_cast(value)); + return text; +} + +std::uint64_t ValueAt(std::uint32_t tid) { + return tid < Values.size() ? Values[tid] : 0u; +} + +void Run(AgcDriver::VulkanDevice& device, const Mode& mode) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + Input[tid * Inputs] = static_cast(ValueAt(tid)); + Input[tid * Inputs + 1u] = static_cast(ValueAt(tid) >> 32u); + Input[tid * Inputs + 2u] = 0u; + Input[tid * Inputs + 3u] = 0u; + } + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const 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; + request.context.floatMode = mode.value; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +std::uint64_t Flush(std::uint64_t bits, bool flushed) { + return flushed && (bits & 0x7ff0000000000000ull) == 0u ? bits & 0x8000000000000000ull : bits; +} + +std::uint64_t Bits(double value) { + return std::bit_cast(value); +} + +std::array Expected(std::uint64_t source, std::uint32_t field) { + const double value = std::bit_cast(Flush(source, (field & 1u) == 0u)); + const double below = std::bit_cast(0x3fefffffffffffffull); + int exponent = 0; + const double mantissa = std::frexp(value, &exponent); + return { + Bits(std::floor(value)), + Bits(std::ceil(value)), + Flush(Bits(std::min(value - std::floor(value), below)), (field & 2u) == 0u), + Bits(mantissa), + static_cast(exponent), + }; +} + +std::uint32_t Check(const Mode& mode, bool keepsDenormals) { + const std::uint32_t field = mode.value.has_value() ? mode.value->floatMode >> 6u : 3u; + const std::array deviceFlushes{field & 2u, field & 1u, 0u}; + constexpr const char* Names[5] = {"v_floor_f64", "v_ceil_f64", "v_fract_f64", "v_frexp_mant_f64", "v_frexp_exp_i32_f64"}; + std::uint32_t skipped = 0; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::uint64_t source = ValueAt(tid); + const auto expected = Expected(source, field); + const std::uint32_t* out = &Output[tid * Results]; + for (std::uint32_t index = 0; index < expected.size(); ++index) { + const std::uint64_t actual = index < 4u ? out[index * 2u] | (static_cast(out[index * 2u + 1u]) << 32u) : out[8]; + if (actual == expected[index]) continue; + const auto text = [index](std::uint64_t bits) { return index < 4u ? Hex(bits) : std::to_string(static_cast(bits)); }; + const std::string message = std::string(Names[index]) + " " + mode.name + " of " + Hex(source) + " is " + text(actual) + ", expected " + text(expected[index]); + const bool deviceDependent = std::any_of(deviceFlushes.begin(), deviceFlushes.end(), [&](std::uint32_t flushed) { return Expected(source, flushed)[index] != expected[index]; }); + Require(deviceDependent && !keepsDenormals, message); + std::printf("skipped %s, the device flushes f64 denormals without DenormPreserve support\n", message.c_str()); + ++skipped; + } + } + return skipped; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!TargetHasCapability(device->Target(), spv::CapabilityFloat64)) { + std::puts("skipped, the device has no shaderFloat64"); + return VulkanTestSkipped; + } + const Mode modes[] = { + {"without a known float mode", std::nullopt}, + {"f64 denormal mode 0, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0x30u, true, true, false}}, + {"f64 denormal mode 0, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0x30u, true, false, false}}, + {"f64 denormal mode 1, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0x70u, true, true, false}}, + {"f64 denormal mode 1, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0x70u, true, false, false}}, + {"f64 denormal mode 2, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0xb0u, true, true, false}}, + {"f64 denormal mode 2, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0xb0u, true, false, false}}, + {"f64 denormal mode 3, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}}, + {"f64 denormal mode 3, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0xf0u, true, false, false}}, + }; + bool keepsDenormals = device->Target().float64DenormPreserve; + std::uint32_t skipped = 0; + for (const Mode& mode : modes) { + Run(*device, mode); + const std::uint32_t modeSkipped = Check(mode, keepsDenormals); + keepsDenormals = keepsDenormals || (!mode.value.has_value() && modeSkipped == 0u); + skipped += modeSkipped; + } + std::printf("f64 rounding denormal mode tests passed, %u device-dependent results skipped\n", skipped); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} 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/GdsAppendConsume.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GdsAppendConsume.cpp index 3be5f5b44a..08d1c78777 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/GdsAppendConsume.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GdsAppendConsume.cpp @@ -30,6 +30,9 @@ constexpr std::size_t ActiveLanesLiteral = 12; constexpr std::size_t OrderedWaveMaskLiteral = 5; constexpr std::size_t OrderedM0Literal = 7; constexpr std::size_t OrderedFirstLaneLiteral = 9; +constexpr std::size_t OrderedOperationLiteral = 10; +constexpr std::uint32_t OrderedAdd = 0xd8fe0310u; +constexpr std::uint32_t OrderedSwap = 0xd8fe1310u; alignas(256) std::array Output{}; alignas(16) std::array CounterReadback{}; @@ -55,7 +58,7 @@ alignas(256) auto OrderedCode = std::to_array({ 0x360400ff, 0x0000003f, 0xbefc03ff, 0x0000ffff, 0x7da604ff, 0x00000000, - 0xd8fe0310, 0x05000001, + OrderedAdd, 0x05000001, 0xbefe04c1, 0xbf8cc07f, 0xe0702000, 0x80000503, @@ -184,6 +187,29 @@ void CheckOrdered(const Workgroup& workgroup, std::uint32_t m0, std::uint32_t fi Require(counters[0] == expected, what + ": GDS counter 0 is " + std::to_string(counters[0]) + ", expected " + std::to_string(expected)); } +void CheckOrderedSwap(const Workgroup& workgroup, std::uint32_t m0, std::uint32_t firstLane, const std::array& counters) { + const auto waves = workgroup.threads / workgroup.waveSize; + const auto what = workgroup.Name() + " ds_ordered_count swap with M0 " + std::to_string(m0) + " from lane " + std::to_string(firstLane) + " of each wave"; + std::vector seen{counters[0]}; + std::vector expected{Starts[0]}; + for (std::uint32_t wave = 0; wave < waves; ++wave) { + const auto first = wave * workgroup.waveSize + firstLane; + for (std::uint32_t lane = wave * workgroup.waveSize; lane < (wave + 1u) * workgroup.waveSize; ++lane) { + const auto actual = Output[lane * Results]; + if (lane < first) { + Require(actual == UntouchedOrdered, what + ": inactive lane " + std::to_string(lane) + " was written " + std::to_string(actual)); + continue; + } + Require(actual == Output[first * Results], what + ": lane " + std::to_string(lane) + " returned " + std::to_string(actual) + ", its wave's first active lane " + std::to_string(Output[first * Results])); + } + seen.push_back(Output[first * Results]); + expected.push_back(first + 3u); + } + std::sort(seen.begin(), seen.end()); + std::sort(expected.begin(), expected.end()); + Require(seen == expected, what + ": the returned values and GDS counter 0 (" + std::to_string(counters[0]) + ") are not the start value and each wave's first active lane value"); +} + void Check(const Workgroup& workgroup, std::uint32_t pass, const std::array& counters) { const auto active = std::min(workgroup.activeLanes, workgroup.threads); const std::array steps{workgroup.threads, workgroup.threads, active, active}; @@ -225,8 +251,13 @@ int main() { OrderedCode[OrderedWaveMaskLiteral] = workgroup.waveSize - 1u; OrderedCode[OrderedM0Literal] = run.m0; OrderedCode[OrderedFirstLaneLiteral] = firstLane; + OrderedCode[OrderedOperationLiteral] = OrderedAdd; Run(*device, workgroup, OrderedCode); CheckOrdered(workgroup, run.m0, firstLane, ReadCounters(queue)); + SetCounters(queue); + OrderedCode[OrderedOperationLiteral] = OrderedSwap; + Run(*device, workgroup, OrderedCode); + CheckOrderedSwap(workgroup, run.m0, firstLane, ReadCounters(queue)); } } std::puts("gds append consume tests passed"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/GuardedNullPointer.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GuardedNullPointer.cpp index d9ad01a30a..172c6a26aa 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/GuardedNullPointer.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GuardedNullPointer.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #ifdef _WIN32 @@ -52,6 +53,12 @@ alignas(256) constexpr std::array ExecCode{ 0x7e02020cu, 0xbefe04c1u, 0xe0701000u, 0x80010102u, 0xbf810000u, }; +alignas(256) constexpr std::array DirectRootCode{ + 0xf4080100u, 0xfa000000u, 0xf4000200u, 0xfa000010u, 0xbe8a0302u, 0xbe8b0303u, 0xbe8c03ffu, 0x11111111u, + 0xbf8cc07fu, 0xbf068008u, 0xbf850003u, 0xf4000305u, 0xfa000000u, 0xbf8cc07fu, 0x7e02020cu, 0x34040082u, + 0xe0701000u, 0x80010102u, 0xbf810000u, +}; + alignas(256) constexpr std::array EmptyExecCode{ 0xf4080100u, 0xfa000000u, 0xf4000200u, 0xfa000010u, 0xf4040280u, 0xfa000018u, 0xbe8c03ffu, 0x11111111u, 0xbf8cc07fu, 0x7e02020cu, 0x34040082u, 0x7da80008u, 0xbf800000u, 0xf4000305u, 0xfa000000u, 0xbf8cc07fu, @@ -64,6 +71,12 @@ alignas(256) constexpr std::array ChainCode{ 0xbf8cc07fu, 0x7e02020cu, 0x34040082u, 0xe0701000u, 0x80010102u, 0xbf810000u, }; +alignas(256) constexpr std::array TwoGuardCode{ + 0xf4080100u, 0xfa000000u, 0xf4040280u, 0xfa000018u, 0xf4040380u, 0xfa000020u, 0xbe8c03ffu, 0x11111111u, + 0xbf8cc07fu, 0xbf068002u, 0xbf840003u, 0xf4000305u, 0xfa000000u, 0xbf820002u, 0xf4000307u, 0xfa000000u, + 0xbf8cc07fu, 0x7e02020cu, 0x34040082u, 0xe0701000u, 0x80010102u, 0xbf810000u, +}; + alignas(256) constexpr std::array DescriptorCode{ 0xf4080100u, 0xfa000000u, 0xf4000200u, 0xfa000010u, 0xf4040280u, 0xfa000018u, 0x7e0202ffu, 0x11111111u, 0xbf8cc07fu, 0xbf068008u, 0xbf850006u, 0xf4080405u, 0xfa000010u, 0xbf8cc07fu, 0xe0300000u, 0x80040100u, @@ -117,7 +130,7 @@ struct Outcome { }; template -Outcome Run(AgcDriver::VulkanDevice& device, const std::array& code, std::uint64_t pointer, std::uint32_t guard) { +Outcome Run(AgcDriver::VulkanDevice& device, const std::array& code, std::uint64_t pointer, std::uint32_t guard, std::uint32_t groups = 1u, std::uint64_t second = 0u, bool direct = false) { const auto output = static_cast(reinterpret_cast(Output.data())); Root = {}; Root[0] = static_cast(output); @@ -127,11 +140,14 @@ Outcome Run(AgcDriver::VulkanDevice& device, const std::array(pointer); Root[7] = static_cast(pointer >> 32u); + Root[8] = static_cast(second); + Root[9] = static_cast(second >> 32u); std::fill(Output.begin(), Output.end(), 0xdeadbeefu); const auto root = static_cast(reinterpret_cast(Root.data())); - const std::array userData{static_cast(root), static_cast(root >> 32u)}; + const std::array words{static_cast(root), static_cast(root >> 32u), static_cast(pointer), static_cast(pointer >> 32u)}; + const std::span userData(words.data(), direct ? 4u : 2u); const std::span span(code); - const ShaderRecompiler::ShaderComputeStageInfo compute{{Lanes, 1, 1}, 0u, {false, false, false}, false, 1}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Lanes, 1, 1}, 0u, {groups > 1u, false, false}, false, 1}; ShaderRecompiler::RecompileRequest request{ {ShaderStage::Compute, reinterpret_cast(span.data()), span, 0, {}}, {64, 0, userData, compute, std::nullopt, std::nullopt, {}}, @@ -150,7 +166,7 @@ Outcome Run(AgcDriver::VulkanDevice& device, const std::array(span.data())); + device.Dispatch(*result, groups, 1, 1, {}, reinterpret_cast(span.data())); device.WaitIdle(); } catch (...) { static_cast(log.Finish()); @@ -178,6 +194,47 @@ void RequireWords(const Outcome& outcome, std::uint32_t lanes, std::uint32_t ins } } +void RunDirectRootTests(AgcDriver::VulkanDevice& device) { + const auto valid = static_cast(reinterpret_cast(Data.data())); + void* reserved = nullptr; +#ifdef _WIN32 + reserved = VirtualAlloc(nullptr, 65536, MEM_RESERVE, PAGE_NOACCESS); +#else + reserved = mmap(nullptr, 65536, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (reserved == MAP_FAILED) reserved = nullptr; +#endif + Require(reserved != nullptr, "direct root: cannot reserve an inaccessible range"); + const auto unmapped = static_cast(reinterpret_cast(reserved)); + const auto taken = Run(device, DirectRootCode, valid, 1u, 1u, 0u, true); + Require(!Faulted(taken) && taken.poisoned == 0u && taken.cacheable, "direct root: a mapped taken load faulted, was poisoned or left the caches:\n" + taken.log); + RequireWords(taken, Lanes, Payload, "direct root: a mapped taken load"); + std::puts("direct root: mapped taken load wrote the payload"); + const auto skippedMapped = Run(device, DirectRootCode, valid, 0u, 1u, 0u, true); + Require(!Faulted(skippedMapped) && skippedMapped.poisoned == 0u && skippedMapped.cacheable, "direct root: a mapped skipped load faulted, was poisoned or left the caches:\n" + skippedMapped.log); + RequireWords(skippedMapped, 0u, 0u, "direct root: a mapped skipped load"); + std::puts("direct root: mapped skipped load wrote the default"); + bool refused = false; + try { + static_cast(Run(device, DirectRootCode, unmapped, 1u, 1u, 0u, true)); + } catch (const std::runtime_error& error) { + char expected[96]; + std::snprintf(expected, sizeof(expected), "guest memory is not readable at 0x%llx", static_cast(unmapped)); + Require(std::string(error.what()).find(expected) != std::string::npos, "direct root: the invalid taken load failed for another reason: " + std::string(error.what())); + refused = true; + } + Require(refused, "direct root: an invalid taken load was accepted"); + std::puts("direct root: inaccessible taken load was rejected"); + const auto skipped = Run(device, DirectRootCode, unmapped, 0u, 1u, 0u, true); + Require(!Faulted(skipped) && skipped.poisoned == 0u && skipped.cacheable, "direct root: an inaccessible skipped load faulted, was poisoned or left the caches:\n" + skipped.log); + RequireWords(skipped, 0u, 0u, "direct root: an inaccessible skipped load"); + std::puts("direct root: inaccessible skipped load wrote the default"); +#ifdef _WIN32 + VirtualFree(reserved, 0, MEM_RELEASE); +#else + munmap(reserved, 65536); +#endif +} + void RunTests(AgcDriver::VulkanDevice& device) { const auto valid = static_cast(reinterpret_cast(Data.data())); void* reserved = nullptr; @@ -194,7 +251,7 @@ void RunTests(AgcDriver::VulkanDevice& device) { Require(!Faulted(taken) && taken.poisoned == 0u && taken.cacheable, "guarded pointer: a mapped pointer faulted, was poisoned or left the caches:\n" + taken.log); RequireWords(taken, Lanes, Payload, "guarded pointer: a mapped pointer under a taken branch"); const auto skippedNull = Run(device, BranchCode, 0u, 0u); - Require(!Faulted(skippedNull) && skippedNull.poisoned == 1u && !skippedNull.cacheable, "guarded pointer: a null pointer behind a branch that skips its read faulted or stayed cacheable:\n" + skippedNull.log); + Require(!Faulted(skippedNull) && skippedNull.poisoned == 0u && skippedNull.cacheable, "guarded pointer: a null pointer behind a branch that skips its read faulted, was poisoned or left the caches:\n" + skippedNull.log); RequireWords(skippedNull, 0u, 0u, "guarded pointer: a skipped read through a null pointer"); const auto takenNull = Run(device, BranchCode, 0u, 1u); Require(takenNull.pipelineVariantId == skippedNull.pipelineVariantId && takenNull.pipelineVariantId == taken.pipelineVariantId && takenNull.variantId == taken.variantId, "guarded pointer: the poison or the branch condition changed the pipeline variant"); @@ -204,6 +261,11 @@ void RunTests(AgcDriver::VulkanDevice& device) { Require(Faulted(takenUnmapped) && takenUnmapped.log.find(Fault(unmapped, 0x2cu)) != std::string::npos, "guarded pointer: a read through an unmapped pointer that runs did not fault at its address:\n" + takenUnmapped.log); Require(takenUnmapped.pipelineVariantId == taken.pipelineVariantId, "guarded pointer: an unmapped pointer changed the pipeline variant"); + const auto twoGuards = Run(device, TwoGuardCode, 0u, 0u, 2u, unmapped); + const bool nullRecord = twoGuards.log.find(Fault(0u, 0x2cu)) != std::string::npos; + const bool unmappedRecord = twoGuards.log.find(Fault(unmapped, 0x38u)) != std::string::npos; + Require(twoGuards.poisoned == 2u && Faulted(twoGuards) && nullRecord != unmappedRecord, "guarded pointer: two workgroups faulting through different guards did not report exactly one of their two records:\n" + twoGuards.log); + const auto someLanes = Run(device, ExecCode, valid, 5u); Require(!Faulted(someLanes), "guarded pointer: a mapped pointer under EXEC faulted:\n" + someLanes.log); RequireWords(someLanes, 5u, Payload, "guarded pointer: a mapped pointer read under EXEC"); @@ -220,7 +282,7 @@ void RunTests(AgcDriver::VulkanDevice& device) { Require(noLanes.pipelineVariantId == someLanes.pipelineVariantId && oneLane.pipelineVariantId == someLanes.pipelineVariantId && emptyNull.pipelineVariantId == emptyMapped.pipelineVariantId, "guarded pointer: the poison changed an EXEC program's pipeline variant"); const auto chainSkipped = Run(device, ChainCode, 0u, 0u); - Require(chainSkipped.poisoned == 3u && Faulted(chainSkipped) && chainSkipped.log.find(Fault(0u, 0x24u)) != std::string::npos, "guarded pointer: a pointer read through a null pointer, used only by a skipped read, did not fault at its own pc:\n" + chainSkipped.log); + Require(chainSkipped.poisoned == 2u && Faulted(chainSkipped) && chainSkipped.log.find(Fault(0u, 0x24u)) != std::string::npos, "guarded pointer: a pointer read through a null pointer, used only by a skipped read, did not fault at its own pc:\n" + chainSkipped.log); const auto chainTaken = Run(device, ChainCode, 0u, 1u); Require(Faulted(chainTaken) && chainTaken.log.find(Fault(0u, 0x24u)) != std::string::npos, "guarded pointer: a read through a pointer read through a null pointer did not fault at the first read:\n" + chainTaken.log); Indirect[0] = valid; @@ -234,7 +296,7 @@ void RunTests(AgcDriver::VulkanDevice& device) { Require(!Faulted(descriptorTaken) && descriptorTaken.poisoned == 0u && descriptorTaken.cacheable, "guarded pointer: a V# behind a mapped pointer faulted, was poisoned or left the caches:\n" + descriptorTaken.log); RequireWords(descriptorTaken, Lanes, Payload, "guarded pointer: a buffer read through a V# behind a mapped pointer"); const auto descriptorSkipped = Run(device, DescriptorCode, 0u, 0u); - Require(!Faulted(descriptorSkipped) && descriptorSkipped.poisoned == 4u && !descriptorSkipped.cacheable, "guarded pointer: a V# behind a branch that skips its null pointer faulted or stayed cacheable:\n" + descriptorSkipped.log); + Require(!Faulted(descriptorSkipped) && descriptorSkipped.poisoned == 0u && descriptorSkipped.cacheable, "guarded pointer: a V# behind a branch that skips its null pointer faulted, was poisoned or left the caches:\n" + descriptorSkipped.log); RequireWords(descriptorSkipped, 0u, 0u, "guarded pointer: a skipped V# load through a null pointer"); const auto descriptorNull = Run(device, DescriptorCode, 0u, 1u); Require(descriptorNull.pipelineVariantId == descriptorSkipped.pipelineVariantId && Faulted(descriptorNull) && descriptorNull.log.find(Fault(0x10u, 0x2cu)) != std::string::npos, "guarded pointer: a V# load through a null pointer that runs did not fault at its pc and address:\n" + descriptorNull.log); @@ -254,12 +316,18 @@ void RunTests(AgcDriver::VulkanDevice& device) { } -int main() { +int main(int argc, char** argv) { try { + Require(argc == 1 || (argc == 2 && std::string(argv[1]) == "--direct-root-only"), "unknown guarded-pointer test arguments"); const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - RunTests(*device); - std::puts("guarded null pointer tests passed"); + if (argc == 2) { + RunDirectRootTests(*device); + std::puts("guarded direct root tests passed"); + } else { + RunTests(*device); + std::puts("guarded null pointer tests passed"); + } return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/GuestMemoryLifetime.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GuestMemoryLifetime.cpp index 9e9e22d814..6c92e2171d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/GuestMemoryLifetime.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GuestMemoryLifetime.cpp @@ -167,8 +167,10 @@ void CheckLockedMutation(AgcDriver::VulkanDevice& device) { } void CheckCopy(AgcDriver::VulkanDevice& device) { - auto* source = Map(); - auto* destination = Map(); + auto* span = static_cast(Map(3 * BlockBytes)); + Require(munmap_nid_postfix(span + BlockBytes, BlockBytes) == 0, "guest copy gap unmap failed"); + void* source = span; + void* destination = span + 2 * BlockBytes; { std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); Fill(device, source); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/HostImportWindows.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/HostImportWindows.cpp index 57396daef2..e6d9bd6b30 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/HostImportWindows.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/HostImportWindows.cpp @@ -3,6 +3,7 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" #include "VulkanTestDevice.hpp" #ifdef _WIN32 #ifndef NOMINMAX @@ -124,6 +125,44 @@ Outcome WindowsFollowTheirRange() { return Outcome::Passed; } +Outcome ResidentMappingsImportAfterCollection() { +#ifdef _WIN32 + constexpr std::size_t bytes = MiB; + auto* data = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, 0x200000)); + Require(data != nullptr, "cannot allocate resident import address"); + const auto section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE | SEC_RESERVE, 0, bytes, nullptr); + Require(section != nullptr, "cannot reserve resident import backing"); + GuestArena::GuestArenaMapDirect_nid_postfix(data, bytes, section, 0, PAGE_READWRITE); + std::memset(data, 0x37, bytes); + GuestAllocations::Mutation().Add(data, bytes, true, true, true); + const auto address = reinterpret_cast(data); + Require(AgcDriver::GuestMemory::CollectWritesUncached(address, bytes) != 0, "resident import memory is not watched"); + { + auto device = OpenVulkanTestDevice(); + Require(device != nullptr, "resident import test lost its Vulkan device"); + const auto context = Probe(*device); + for (unsigned int pass = 0; pass < 2; ++pass) { + Require(AgcDriver::GuestMemory::CollectWritesUncached(address, bytes) != 0, "resident import collection failed"); + Require(Fill(*device, address), "resident memory could not be filled after collecting its writes"); + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + Require(Graphics::HostImportCovers(context, address, ProbeBytes), "resident import did not cover its data"); + Graphics::ClearHostImports(device->Device()); + } + Require(Fill(*device, address), "resident memory could not be filled after collecting its writes"); + std::uint32_t first = 0; + std::memcpy(&first, data, sizeof(first)); + Require(first == 0x11111111u && data[ProbeBytes] == 0x37 && data[bytes - 1] == 0x37, "resident import lost the GPU fill or neighboring bytes"); + Graphics::ClearHostImports(device->Device()); + } + GuestAllocations::Mutation().Remove(data); + GuestArena::GuestArenaReset_nid_postfix(data, bytes); + GuestArena::GuestArenaRelease_nid_postfix(data, bytes); + GuestArena::GuestArenaForgetSection_nid_postfix(section); + CloseHandle(section); +#endif + return Outcome::Passed; +} + } int main() { @@ -136,6 +175,7 @@ int main() { } const auto windows = WindowsFollowTheirRange(); Require(windows == Outcome::Passed, "host import windows: the device stopped importing guest memory between two scenarios"); + Require(ResidentMappingsImportAfterCollection() == Outcome::Passed, "resident import test failed"); std::puts("host import windows tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageArraySampleOnVolume.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageArraySampleOnVolume.cpp new file mode 100644 index 0000000000..958a05d7ba --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageArraySampleOnVolume.cpp @@ -0,0 +1,224 @@ +#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 Words = 32; +constexpr std::uint32_t Size = 16; +constexpr std::uint32_t Format8888UNorm = 56; +constexpr std::uint32_t Type3D = 10; +constexpr std::uint32_t Depth = 4; +constexpr std::uint32_t SampleResult = 8; +constexpr std::uint32_t LoadResult = 12; +alignas(256) std::array Buffer{}; +alignas(256) std::array Volume{}; + +alignas(256) constexpr std::array ArraySampleCode{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301008, 0x80000401, 0xe030100c, + 0x80000501, 0xe0301010, 0x80000601, 0xe0301014, 0x80000701, 0xbf8c3f70, 0xf09c0f28, 0x00610c02, + 0xf0001f10, 0x00011005, 0xbf8c3f70, 0xe0701020, 0x80000c01, 0xe0701024, 0x80000d01, 0xe0701028, + 0x80000e01, 0xe070102c, 0x80000f01, 0xe0701030, 0x80001001, 0xe0701034, 0x80001101, 0xe0701038, + 0x80001201, 0xe070103c, 0x80001301, 0xbf810000, +}; + +alignas(256) constexpr std::array VolumeSampleCode{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301008, 0x80000401, 0xe030100c, + 0x80000501, 0xe0301010, 0x80000601, 0xe0301014, 0x80000701, 0xbf8c3f70, 0xf09c0f10, 0x00610c02, + 0xf0001f10, 0x00011005, 0xbf8c3f70, 0xe0701020, 0x80000c01, 0xe0701024, 0x80000d01, 0xe0701028, + 0x80000e01, 0xe070102c, 0x80000f01, 0xe0701030, 0x80001001, 0xe0701034, 0x80001101, 0xe0701038, + 0x80001201, 0xe070103c, 0x80001301, 0xbf810000, +}; + +alignas(256) constexpr std::array ArrayExplicitLodCode{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301008, 0x80000401, 0xe030100c, + 0x80000501, 0xbf8c3f70, 0xf0900f28, 0x00610c02, 0xbf8c3f70, 0xe0701020, 0x80000c01, 0xe0701024, + 0x80000d01, 0xe0701028, 0x80000e01, 0xe070102c, 0x80000f01, 0xbf810000, +}; + +alignas(256) constexpr std::array VolumeExplicitLodCode{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301008, 0x80000401, 0xe030100c, + 0x80000501, 0xbf8c3f70, 0xf0900f10, 0x00610c02, 0xbf8c3f70, 0xe0701020, 0x80000c01, 0xe0701024, + 0x80000d01, 0xe0701028, 0x80000e01, 0xe070102c, 0x80000f01, 0xbf810000, +}; + +alignas(256) constexpr std::array ArrayBiasCode{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301008, 0x80000401, 0xe030100c, + 0x80000501, 0xbf8c3f70, 0xf0940f28, 0x00610c02, 0xbf8c3f70, 0xe0701020, 0x80000c01, 0xe0701024, + 0x80000d01, 0xe0701028, 0x80000e01, 0xe070102c, 0x80000f01, 0xbf810000, +}; + +struct Lane { + std::uint32_t x; + std::uint32_t y; + std::uint32_t z; + float r; +}; + +Lane LaneOf(std::uint32_t tid) { + const auto z = tid % Depth; + const auto wraps = static_cast((tid / Depth) % 4u) - 1.0f; + return {(tid * 7u + 3u) % Size, (tid * 5u + 1u) % Size, z, (static_cast(z) + 0.5f) / static_cast(Depth) + wraps}; +} + +std::uint32_t Bits(float value) { + return std::bit_cast(value); +} + +void FillInput(bool explicitLod, float lod) { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto lane = LaneOf(tid); + const auto u = Bits((static_cast(lane.x) + 0.5f) / static_cast(Size)); + const auto v = Bits((static_cast(lane.y) + 0.5f) / static_cast(Size)); + const std::array words{u, v, Bits(lane.r), explicitLod ? Bits(lod) : lane.x, lane.y, lane.z}; + std::copy(words.begin(), words.end(), &Buffer[tid * Words]); + } +} + +std::uint8_t Marker(std::uint32_t level, std::uint32_t z) { + return static_cast(level * 25u + z * 50u + 7u); +} + +std::array VolumeDescriptor(std::uint32_t levels) { + const auto address = static_cast(reinterpret_cast(Volume.data())); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (((Size - 1u) & 3u) << 30u), + ((Size - 1u) >> 2u) | ((Size - 1u) << 14u), + 0xfacu | ((levels - 1u) << 16u) | (Type3D << 28u), + Depth - 1u, + (levels - 1u) << 4u, + 0u, + 0u, + }; +} + +void FillVolume(std::uint32_t levels) { + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(VolumeDescriptor(levels))); + Require(geometry.guestBytes <= Volume.size(), "image array sample on a volume: the texture does not fit the texel storage"); + std::fill(Volume.begin(), Volume.end(), 0xeeu); + for (std::uint32_t level = 0; level < levels; ++level) { + const auto& mip = geometry.mips.at(level); + for (std::uint32_t z = 0; z < Depth; ++z) { + if (!geometry.HasLayer(level, z)) continue; + for (std::uint32_t y = 0; y < mip.height; ++y) { + for (std::uint32_t x = 0; x < mip.width; ++x) { + auto* texel = &Volume[geometry.GuestLayerOffset(z) + mip.tiledOffset + static_cast(y) * mip.pitchBytes + x * 4u]; + texel[0] = Marker(level, z); + texel[1] = static_cast(x); + texel[2] = static_cast(y); + texel[3] = 255u; + } + } + } + } +} + +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 SamplerDescriptor() { + return {0u, 0xfffu << 12u, 1u << 26u, 0u}; +} + +void Run(AgcDriver::VulkanDevice& device, std::span code, std::uint32_t levels) { + std::vector userData(16, 0u); + const auto buffer = BufferDescriptor(Buffer.data(), static_cast(Buffer.size() * 4u)); + const auto texture = VolumeDescriptor(levels); + const auto sampler = SamplerDescriptor(); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + std::copy(sampler.begin(), sampler.end(), userData.begin() + 12); + const std::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(); +} + +void Expect(std::uint32_t tid, std::uint32_t result, const std::array& expected, const std::string& name) { + for (std::uint32_t component = 0; component < 4u; ++component) { + const float value = std::bit_cast(Buffer[tid * Words + result + component]) * 255.0f; + Require(std::lround(value) == static_cast(expected[component]), name + ": thread " + std::to_string(tid) + " (r " + std::to_string(LaneOf(tid).r) + ") component " + std::to_string(component) + " is " + std::to_string(value) + ", expected " + std::to_string(expected[component])); + } +} + +std::array Texel(std::uint32_t tid, std::uint32_t level) { + const auto lane = LaneOf(tid); + return {Marker(level, lane.z >> level), lane.x >> level, lane.y >> level, 255u}; +} + +void CheckSamples(const std::string& name, std::uint32_t level, bool load) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + Expect(tid, SampleResult, Texel(tid, level), name); + if (load) Expect(tid, LoadResult, Texel(tid, 0u), "image_load 3d on a 3D texture"); + } +} + +void RequireRefused(AgcDriver::VulkanDevice& device) { + std::string refusal; + try { + Run(device, ArrayBiasCode, 1u); + } catch (const std::exception& error) { + refusal = error.what(); + } + Require(refusal.find("incompatible with the static runtime image interface") != std::string::npos, "image_sample_b 2d_array on a 3D texture was not refused: " + refusal); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + FillVolume(1u); + FillInput(false, 0.0f); + Run(*device, VolumeSampleCode, 1u); + CheckSamples("image_sample_lz 3d on a 3D texture", 0u, true); + FillInput(false, 0.0f); + Run(*device, ArraySampleCode, 1u); + CheckSamples("image_sample_lz 2d_array on a 3D texture", 0u, true); + FillInput(true, 0.0f); + Run(*device, ArrayExplicitLodCode, 1u); + CheckSamples("image_sample_l 2d_array on a 3D texture", 0u, false); + FillVolume(2u); + FillInput(true, 1.0f); + Run(*device, VolumeExplicitLodCode, 2u); + CheckSamples("image_sample_l 3d with LOD 1 on a two-level 3D texture", 1u, false); + FillInput(true, 1.0f); + Run(*device, ArrayExplicitLodCode, 2u); + CheckSamples("image_sample_l 2d_array with LOD 1 on a two-level 3D texture", 1u, false); + RequireRefused(*device); + std::puts("image array sample on volume tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/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/ImageMsaaAddressOnPlane.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageMsaaAddressOnPlane.cpp new file mode 100644 index 0000000000..a2d0b12593 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageMsaaAddressOnPlane.cpp @@ -0,0 +1,115 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { +using AgcDriver::Graphics::Require; +constexpr std::uint32_t Threads = 64; +constexpr std::uint32_t Words = 8; +alignas(256) std::array buffer{}; +alignas(256) std::array texture{}; +constexpr std::array Coordinates{0u, 1u, 3u, 4u}; +constexpr std::array Samples{0u, 1u, 7u, 0xffffffffu}; +constexpr std::array UnormExpected{0x0u, 0x3b808081u, 0x3c40c0c1u, 0x3e808081u, 0x3e828283u, 0x3e868687u, 0x3f40c0c1u, 0x3f41c1c2u, 0x3f43c3c4u}; +constexpr std::array SrgbExpected{0x0u, 0x399f0000u, 0x3a6f0000u, 0x3d520000u, 0x3d590000u, 0x3d660000u, 0x3f070000u, 0x3f090000u, 0x3f0c0000u}; +alignas(256) constexpr std::array MsaaCode{ + 0x34020085u, 0xe0301000u, 0x80000201u, 0xe0301004u, 0x80000301u, 0xe0301008u, 0x80000401u, + 0xbf8c3f70u, 0xf0000130u, 0x00010802u, 0xbf8c3f70u, 0xe0701010u, 0x80000801u, 0xbf810000u +}; +alignas(256) constexpr auto PlaneCode = [] { + auto code = MsaaCode; + code[8] = 0xf0000108u; + return code; +}(); + +std::array TextureDescriptor(std::uint32_t format) { + const auto address = reinterpret_cast(texture.data()); + return {static_cast(address >> 8u), static_cast((address >> 40u) & 0xffu) | (format << 20u) | 0xc0000000u, 0xc000u, 0x90000facu, 0u, 0u, 0u, 0u}; +} + +void FillTexture(const std::array& descriptor) { + const auto layout = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(descriptor)); + Require(layout.guestBytes <= texture.size(), "MSAA-address texture exceeds its storage"); + texture.fill(std::byte{0xee}); + const auto& mip = layout.mips.at(0); + for (std::uint32_t y = 0; y < 4; ++y) { + for (std::uint32_t x = 0; x < 4; ++x) { + const auto value = 0x12340000u + y * 64u + x; + std::memcpy(texture.data() + mip.tiledOffset + y * mip.pitchBytes + x * 4u, &value, sizeof(value)); + } + } +} + +std::array Load(AgcDriver::VulkanDevice& device, const std::array& image, std::span code, std::uint32_t wave) { + buffer.fill(0xdeadbeefu); + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + buffer[lane * Words] = Coordinates[lane % 4u]; + buffer[lane * Words + 1u] = Coordinates[(lane / 4u) % 4u]; + buffer[lane * Words + 2u] = Samples[lane / 16u]; + } + const auto address = reinterpret_cast(buffer.data()); + std::vector data{static_cast(address), static_cast((address >> 32u) & 0xffffu), static_cast(buffer.size() * 4u), 0x31016facu}; + data.insert(data.end(), image.begin(), image.end()); + 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{ + {ShaderRecompiler::ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {wave, 0, data, compute, std::nullopt, std::nullopt, memory}, device.Target(), {0, 0, 0, 128} + }; + request.useCache = false; + const auto compiled = ShaderRecompiler::Recompile(request); + device.Dispatch(compiled, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + std::array result{}; + for (std::uint32_t lane = 0; lane < Threads; ++lane) result[lane] = buffer[lane * Words + 4u]; + return result; +} + +void Run(AgcDriver::VulkanDevice& device) { + for (const auto format : {20u, 22u, 56u, 130u}) { + const auto descriptor = TextureDescriptor(format); + FillTexture(descriptor); + for (const auto wave : {32u, 64u}) { + const auto plain = Load(device, descriptor, PlaneCode, wave); + const auto msaa = Load(device, descriptor, MsaaCode, wave); + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + Require(msaa[lane] == plain[lane], "MSAA-address load differs from the measured 2D control at lane " + std::to_string(lane)); + const auto x = Coordinates[lane % 4u]; + const auto y = Coordinates[(lane / 4u) % 4u]; + if (x == 4u || y == 4u) Require(msaa[lane] == 0u, "MSAA-address out-of-range load did not read zero"); + else if (format == 20u || format == 22u) Require(msaa[lane] == 0x12340000u + y * 64u + x, "MSAA-address load lost the oracle texel marker"); + else { + const auto index = ((lane / 4u) % 4u) * 3u + lane % 4u; + const auto expected = std::bit_cast((format == 56u ? UnormExpected : SrgbExpected)[index]); + const auto actual = std::bit_cast(msaa[lane]); + if (format == 130u) { + const auto encoded = [](float linear) { return std::lround((linear <= 0.0031308f ? linear * 12.92f : 1.055f * std::pow(linear, 1.0f / 2.4f) - 0.055f) * 255.0f); }; + Require(std::isfinite(actual) && encoded(actual) == encoded(expected), "MSAA-address sRGB load changed the oracle's encoded texel"); + } else Require(std::abs(actual - expected) < 0.0001f, "MSAA-address normalized load differs from the Radeon oracle"); + } + } + } + } +} +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Run(*device); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageOneDimensionalCompare.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageOneDimensionalCompare.cpp new file mode 100644 index 0000000000..b119ee834f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageOneDimensionalCompare.cpp @@ -0,0 +1,365 @@ +#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 + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Words = 32; +constexpr std::uint32_t Size = 16; +constexpr std::uint32_t Layers = 3; +constexpr std::uint32_t Format16UNorm = 7; +constexpr std::uint32_t Type1D = 8; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t Type1DArray = 12; +constexpr std::uint32_t ReferenceWord = 6; +constexpr std::uint32_t Result = 8; +constexpr std::uint32_t Step1D = 4000; +constexpr std::uint32_t StepArray = 1200; +constexpr std::uint32_t Step2D = 200; +constexpr std::uint32_t Row2D = 2; +constexpr std::uint32_t CompareLess = 1; + +alignas(256) std::array Buffer{}; +alignas(256) std::array Line{}; +alignas(4096) std::array ArrayTexels{}; +alignas(4096) std::array PlaneTexels{}; + +alignas(256) constexpr std::array Code1D_Lz{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301018, 0x80000801, 0xbf8c3f70, + 0x7e280308, 0x7e2a0302, 0xf0bc0100, 0x00610c14, 0xbf8c3f70, 0xe0701020, + 0x80000c01, 0xbf810000, 0xbf810000, 0xbf810000, +}; + +alignas(256) constexpr std::array Code1D_ImplicitLod{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301018, 0x80000801, 0xbf8c3f70, + 0x7e280308, 0x7e2a0302, 0xf0a00100, 0x00610c14, 0xbf8c3f70, 0xe0701020, + 0x80000c01, 0xbf810000, 0xbf810000, 0xbf810000, +}; + +alignas(256) constexpr std::array Code1D_ExplicitLod{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301018, 0x80000801, 0xbf8c3f70, + 0x7e280308, 0x7e2a0302, 0x7e2c0280, 0xf0b00100, 0x00610c14, 0xbf8c3f70, + 0xe0701020, 0x80000c01, 0xbf810000, 0xbf810000, 0xbf810000, +}; + +alignas(256) constexpr std::array Code1D_Lz_Offset{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301018, 0x80000801, 0xbf8c3f70, + 0x7e2802ff, 0x00000f00, 0x7e2a0308, 0x7e2c0302, 0xf0fc0100, 0x00610c14, + 0xbf8c3f70, 0xe0701020, 0x80000c01, 0xbf810000, 0xbf810000, 0xbf810000, +}; + +alignas(256) constexpr std::array Code1DArray_Lz_Offset{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301018, 0x80000801, 0xbf8c3f70, + 0x7e2802ff, 0x00000f00, 0x7e2a0308, 0x7e2c0302, 0x7e2e0303, 0xf0fc0120, 0x00610c14, + 0xbf8c3f70, 0xe0701020, 0x80000c01, 0xbf810000, +}; + +alignas(256) constexpr std::array Code1DArray_Lz{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301018, 0x80000801, 0xbf8c3f70, + 0x7e280308, 0x7e2a0302, 0x7e2c0303, 0xf0bc0120, 0x00610c14, 0xbf8c3f70, 0xe0701020, 0x80000c01, + 0xbf810000, +}; + +alignas(256) constexpr std::array Code1DArray_ImplicitLod{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301018, 0x80000801, 0xbf8c3f70, + 0x7e280308, 0x7e2a0302, 0x7e2c0303, 0xf0a00120, 0x00610c14, 0xbf8c3f70, 0xe0701020, 0x80000c01, + 0xbf810000, +}; + +alignas(256) constexpr std::array Code2D_Lz{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301018, 0x80000801, 0xbf8c3f70, + 0x7e280308, 0x7e2a0302, 0x7e2c0303, 0xf0bc0108, 0x00610c14, 0xbf8c3f70, 0xe0701020, 0x80000c01, + 0xbf810000, +}; + +alignas(256) constexpr std::array Code2D_ImplicitLod{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301018, 0x80000801, 0xbf8c3f70, + 0x7e280308, 0x7e2a0302, 0x7e2c0303, 0xf0a00108, 0x00610c14, 0xbf8c3f70, 0xe0701020, 0x80000c01, + 0xbf810000, +}; + +alignas(256) constexpr std::array Code1D_Gather_C_Lz{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301018, 0x80000801, 0xbf8c3f70, + 0x7e280308, 0x7e2a0302, 0xf13c0100, 0x00610c14, 0xbf8c3f70, 0xe0701020, + 0x80000c01, 0xbf810000, 0xbf810000, 0xbf810000, +}; + +alignas(256) constexpr std::array Code1DArray_Gather_C_Lz{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301018, 0x80000801, 0xbf8c3f70, + 0x7e280308, 0x7e2a0302, 0x7e2c0303, 0xf13c0120, 0x00610c14, 0xbf8c3f70, 0xe0701020, 0x80000c01, + 0xbf810000, +}; + +std::uint32_t TexelOf(std::uint32_t tid) { + return (tid * 7u + 3u) % Size; +} + +bool ReferenceBelow(std::uint32_t tid) { + return (tid & 1u) == 0u; +} + +std::uint32_t Bits(float value) { + return std::bit_cast(value); +} + +void FillInput1D() { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto x = TexelOf(tid); + const float texel = static_cast((x + 1u) * Step1D) / 65535.0f; + const float half = static_cast(Step1D / 2u) / 65535.0f; + Buffer[tid * Words] = Bits((static_cast(x) + 0.5f) / static_cast(Size)); + Buffer[tid * Words + ReferenceWord] = Bits(ReferenceBelow(tid) ? texel - half : texel + half); + } +} + +void FillInput1DArray() { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto x = TexelOf(tid); + const auto layer = (tid / 7u) % Layers; + const float texel = static_cast((layer * Size + x + 1u) * StepArray) / 65535.0f; + const float half = static_cast(StepArray / 2u) / 65535.0f; + Buffer[tid * Words] = Bits((static_cast(x) + 0.5f) / static_cast(Size)); + Buffer[tid * Words + 1u] = Bits(static_cast(layer)); + Buffer[tid * Words + ReferenceWord] = Bits(ReferenceBelow(tid) ? texel - half : texel + half); + } +} + +void FillInput2D() { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto x = TexelOf(tid); + const float texel = static_cast((Row2D * Size + x + 1u) * Step2D) / 65535.0f; + const float half = static_cast(Step2D / 2u) / 65535.0f; + Buffer[tid * Words] = Bits((static_cast(x) + 0.5f) / static_cast(Size)); + Buffer[tid * Words + 1u] = Bits((static_cast(Row2D) + 0.5f) / static_cast(Size)); + Buffer[tid * Words + ReferenceWord] = Bits(ReferenceBelow(tid) ? texel - half : texel + half); + } +} + +void FillInputArrayFirstLayer() { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto x = TexelOf(tid); + const float texel = static_cast((0u * Size + x + 1u) * StepArray) / 65535.0f; + const float half = static_cast(StepArray / 2u) / 65535.0f; + Buffer[tid * Words] = Bits((static_cast(x) + 0.5f) / static_cast(Size)); + Buffer[tid * Words + ReferenceWord] = Bits(ReferenceBelow(tid) ? texel - half : texel + half); + } +} + +std::array LineDescriptor() { + const auto address = static_cast(reinterpret_cast(Line.data())); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format16UNorm << 20u) | (((Size - 1u) & 3u) << 30u), + (Size - 1u) >> 2u, + 0xfacu | (Type1D << 28u), + 0u, 0u, 0u, 0u, + }; +} + +std::array ArrayDescriptor() { + const auto address = static_cast(reinterpret_cast(ArrayTexels.data())); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format16UNorm << 20u) | (((Size - 1u) & 3u) << 30u), + (Size - 1u) >> 2u, + 0xfacu | (Type1DArray << 28u), + Layers - 1u, 0u, 0u, 0u, + }; +} + +std::array PlaneDescriptor() { + const auto address = static_cast(reinterpret_cast(PlaneTexels.data())); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format16UNorm << 20u) | (((Size - 1u) & 3u) << 30u), + ((Size - 1u) >> 2u) | ((Size - 1u) << 14u), + 0xfacu | (Type2D << 28u), + 0u, 0u, 0u, 0u, + }; +} + +void FillLine() { + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(LineDescriptor())); + Require(geometry.guestBytes <= Line.size(), "1D texture does not fit texel storage"); + std::fill(Line.begin(), Line.end(), 0xeeu); + for (std::uint32_t x = 0; x < Size; ++x) { + const auto value = static_cast((x + 1u) * Step1D); + auto* texel = &Line[geometry.GuestLayerOffset(0) + geometry.mips.at(0).tiledOffset + x * 2u]; + texel[0] = static_cast(value); + texel[1] = static_cast(value >> 8u); + } +} + +void FillArray() { + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(ArrayDescriptor())); + Require(geometry.guestBytes <= ArrayTexels.size(), "1D array texture does not fit texel storage"); + std::fill(ArrayTexels.begin(), ArrayTexels.end(), 0xeeu); + for (std::uint32_t layer = 0; layer < Layers; ++layer) { + for (std::uint32_t x = 0; x < Size; ++x) { + const auto value = static_cast((layer * Size + x + 1u) * StepArray); + auto* texel = &ArrayTexels[geometry.GuestLayerOffset(layer) + geometry.mips.at(0).tiledOffset + x * 2u]; + texel[0] = static_cast(value); + texel[1] = static_cast(value >> 8u); + } + } +} + +void FillPlane() { + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(PlaneDescriptor())); + Require(geometry.guestBytes <= PlaneTexels.size(), "2D texture does not fit texel storage"); + std::fill(PlaneTexels.begin(), PlaneTexels.end(), 0xeeu); + for (std::uint32_t y = 0; y < Size; ++y) { + for (std::uint32_t x = 0; x < Size; ++x) { + const auto value = static_cast((y * Size + x + 1u) * Step2D); + auto* texel = &PlaneTexels[geometry.GuestLayerOffset(0) + geometry.mips.at(0).tiledOffset + static_cast(y) * geometry.mips.at(0).pitchBytes + x * 2u]; + texel[0] = static_cast(value); + texel[1] = static_cast(value >> 8u); + } + } +} + +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 SamplerDescriptor() { + return {CompareLess << 12u, 0xfffu << 12u, 1u << 26u, 0u}; +} + +void Run(AgcDriver::VulkanDevice& device, std::span code, const std::array& textureDesc) { + std::vector userData(16, 0u); + const auto buffer = BufferDescriptor(Buffer.data(), static_cast(Buffer.size() * 4u)); + const auto sampler = SamplerDescriptor(); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(textureDesc.begin(), textureDesc.end(), userData.begin() + 4); + std::copy(sampler.begin(), sampler.end(), userData.begin() + 12); + 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(); +} + +void Check(const std::string& op) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const float value = std::bit_cast(Buffer[tid * Words + Result]); + const float expected = ReferenceBelow(tid) ? 1.0f : 0.0f; + Require(value == expected, op + ": thread " + std::to_string(tid) + " is " + std::to_string(value) + ", expected " + std::to_string(expected)); + } +} + +void ExpectGatherReject(AgcDriver::VulkanDevice& device, std::span code, const std::array& textureDesc) { + std::vector userData(16, 0u); + const auto buffer = BufferDescriptor(Buffer.data(), static_cast(sizeof(Buffer))); + const auto sampler = SamplerDescriptor(); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(textureDesc.begin(), textureDesc.end(), userData.begin() + 4); + std::copy(sampler.begin(), sampler.end(), userData.begin() + 12); + 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; + try { + (void)ShaderRecompiler::Recompile(request); + Require(false, "remapped 1D depth gather was accepted unexpectedly"); + } catch (const std::exception& error) { + Require(std::string(error.what()).find("has an unsupported 1D gather variant") != std::string::npos, "unexpected error for 1D depth gather: " + std::string(error.what())); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + FillLine(); + FillArray(); + FillPlane(); + + FillInput1D(); + Run(*device, Code1D_Lz, LineDescriptor()); + Check("1D image_sample_c_lz"); + + FillInput1D(); + Run(*device, Code1D_ImplicitLod, LineDescriptor()); + Check("1D image_sample_c"); + + FillInput1D(); + Run(*device, Code1D_ExplicitLod, LineDescriptor()); + Check("1D image_sample_c_l"); + + FillInput1D(); + Run(*device, Code1D_Lz_Offset, LineDescriptor()); + Check("1D image_sample_c_lz_o"); + + FillInput1DArray(); + Run(*device, Code1DArray_Lz, ArrayDescriptor()); + Check("1D array image_sample_c_lz"); + + FillInput1DArray(); + Run(*device, Code1DArray_Lz_Offset, ArrayDescriptor()); + Check("1D array image_sample_c_lz_o"); + + FillInput1DArray(); + Run(*device, Code1DArray_ImplicitLod, ArrayDescriptor()); + Check("1D array image_sample_c"); + + FillInputArrayFirstLayer(); + Run(*device, Code1D_Lz, ArrayDescriptor()); + Check("1D image_sample_c_lz on 1D array first layer"); + + FillInputArrayFirstLayer(); + Run(*device, Code1D_ImplicitLod, ArrayDescriptor()); + Check("1D image_sample_c on 1D array first layer"); + + FillInput2D(); + Run(*device, Code2D_Lz, PlaneDescriptor()); + Check("2D image_sample_c_lz"); + + FillInput2D(); + Run(*device, Code2D_ImplicitLod, PlaneDescriptor()); + Check("2D image_sample_c"); + + ExpectGatherReject(*device, Code1D_Gather_C_Lz, LineDescriptor()); + ExpectGatherReject(*device, Code1DArray_Gather_C_Lz, ArrayDescriptor()); + + std::puts("1D, 1D-array, and 2D comparison tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImagePackedHorizontalGather.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImagePackedHorizontalGather.cpp index 336a066bf8..9621ea1f12 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImagePackedHorizontalGather.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImagePackedHorizontalGather.cpp @@ -16,6 +16,12 @@ namespace { +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + using AgcDriver::Graphics::Require; using ShaderRecompiler::ShaderStage; @@ -156,7 +162,7 @@ void Run(AgcDriver::VulkanDevice& device, std::uint32_t dmask) { const auto expected = Expected(tid, dmask); for (std::uint32_t component = 0; component < static_cast(std::popcount(dmask)); ++component) { const std::uint32_t actual = Output[tid * 4u + component]; - Require(actual == expected[component], std::string("packed horizontal gather: dmask ") + std::to_string(dmask) + " thread " + std::to_string(tid) + " register " + std::to_string(component) + " is 0x" + std::to_string(actual) + ", expected 0x" + std::to_string(expected[component])); + Require(actual == expected[component], std::string("packed horizontal gather: dmask ") + std::to_string(dmask) + " thread " + std::to_string(tid) + " register " + std::to_string(component) + " is " + Hex(actual) + ", expected " + Hex(expected[component])); } } } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleForceDegamma.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleForceDegamma.cpp index bab71fa850..331882d14a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleForceDegamma.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleForceDegamma.cpp @@ -25,6 +25,8 @@ constexpr std::uint32_t Size = 4; constexpr std::uint32_t Format8888UNorm = 56; constexpr std::uint32_t Format8888Srgb = 130; constexpr std::uint32_t Format32Float = 22; +constexpr std::uint32_t Format8888SNorm = 57; +constexpr std::uint32_t FormatBc4UNorm = 175; constexpr std::uint32_t Type2D = 9; constexpr std::uint32_t FilterPoint = 0; constexpr std::uint32_t FilterBilinear = 1; @@ -173,7 +175,9 @@ int main() { Require(unormPlain[0] != unormPoint[0] || unormPlain[4] != unormPoint[4], "8_8_8_8_UNORM without FORCE_DEGAMMA sampled the decoded values"); FillInput(false); RequireSame(Sample(*device, Format8888UNorm, FilterBilinear, ForceDegamma), forcedBilinear, "8_8_8_8_UNORM, bilinear", "through FORCE_DEGAMMA", "as 8_8_8_8_SRGB"); - Reject(*device, Format32Float, "guest format 22, which is not sRGB and has no sRGB view, is sampled through a sampler that forces sRGB decoding"); + RequireSame(Sample(*device, Format32Float, FilterPoint, ForceDegamma), Sample(*device, Format32Float, FilterPoint, 0u), "32_FLOAT, point"); + RequireSame(Sample(*device, FormatBc4UNorm, FilterPoint, ForceDegamma), Sample(*device, FormatBc4UNorm, FilterPoint, 0u), "BC4_UNORM, point"); + Reject(*device, Format8888SNorm, "guest format 57, which is not sRGB and has no sRGB view, is sampled through a sampler that forces sRGB decoding"); std::puts("image sample force degamma tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreUnormRounding.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreUnormRounding.cpp new file mode 100644 index 0000000000..bb6c6f25cf --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreUnormRounding.cpp @@ -0,0 +1,264 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Width = 32; +constexpr std::uint32_t Height = 8; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t IdentitySwizzle = 0xfacu; +constexpr std::uint32_t TwoChannelSwizzle = 0x22cu; +constexpr std::size_t TexelBytes = 8192; +constexpr std::size_t BlockBytes = 65536; + +alignas(256) std::array Input{}; + +alignas(256) constexpr std::array StoreCode{ + 0x34020084, 0xe0381000, 0x80001401, 0xbf8c3f70, 0x7e3c0300, 0x7e3e0280, 0xf0201f08, 0x0001141e, + 0x7e3e0281, 0xf0201508, 0x0001141e, 0xbf810000, +}; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); +#endif + Require(block != nullptr, "image store unorm rounding: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; +}; + + +struct Format { + const char* name; + std::uint32_t format; + std::uint32_t components; + std::uint32_t bits; + bool snorm; +}; + +constexpr std::array Formats{{ + {"8_UNORM", 1u, 1u, 8u, false}, + {"16_16_UNORM", 23u, 2u, 16u, false}, + {"8_8_8_8_UNORM", 56u, 4u, 8u, false}, + {"8_8_8_8_SNORM", 57u, 4u, 8u, true}, + {"16_16_16_16_UNORM", 65u, 4u, 16u, false}, +}}; + +struct Write { + std::uint32_t row; + std::uint32_t dmask; +}; + +constexpr std::array StoreWrites{{{0u, 0xfu}, {1u, 0x5u}}}; + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +float Scale(const Format& format) { + return static_cast((1u << (format.snorm ? format.bits - 1u : format.bits)) - 1u); +} + +float Value(const Format& format, std::uint32_t tid, std::uint32_t index) { + constexpr std::array Edges{0.0f, -0.0f, 1.0f, 0.5f, -0.25f, 1.5f, -1.5f, 1e-30f, -1e-30f, 0.999f, 0.0019607844f, 0.9980392f, 0.33333334f, -0.6666667f, 2e-5f, 0.75f}; + const auto slot = tid * 4u + index; + if (slot < Edges.size()) return Edges[slot]; + if (slot == Edges.size()) return std::numeric_limits::quiet_NaN(); + if (slot == Edges.size() + 1u) return std::numeric_limits::infinity(); + if (slot == Edges.size() + 2u) return -std::numeric_limits::infinity(); + const auto scale = Scale(format); + const auto level = static_cast((slot * 37u) % static_cast(scale)); + auto value = (level + 0.5f) / scale; + for (std::uint32_t step = 0; step < slot % 5u; ++step) value = std::nextafter(value, slot % 2u == 0u ? 2.0f : -2.0f); + return format.snorm && slot % 3u == 0u ? -value : value; +} + +std::uint32_t Level(const Format& format, float value) { + const auto lower = format.snorm ? -1.0f : 0.0f; + const auto clamped = std::isnan(value) ? 0.0f : std::clamp(value, lower, 1.0f); + const auto product = static_cast(clamped) * static_cast(Scale(format)); + const auto level = static_cast(std::nearbyint(product)); + return static_cast(level) & ((1u << format.bits) - 1u); +} + +std::uint32_t TexelBytesOf(const Format& format) { + return format.components * format.bits / 8u; +} + +std::uint32_t SwizzleOf(const Format& format) { + return format.components == 2u ? TwoChannelSwizzle : IdentitySwizzle; +} + +AgcDriver::Graphics::TileMipLayout Mip(const Format& format) { + return AgcDriver::Graphics::ComputeMipLayout(AgcDriver::Graphics::TextureTileMode::kLinear, format.format, Width, Height, 1u).at(0); +} + +std::uint64_t TexelOffset(const Format& format, std::uint32_t x, std::uint32_t y) { + const auto mip = Mip(format); + return mip.tiledOffset + static_cast(y) * mip.pitchBytes + static_cast(x) * TexelBytesOf(format); +} + +std::vector Initial() { + std::vector bytes(TexelBytes); + for (std::size_t index = 0; index < bytes.size(); ++index) { + bytes[index] = static_cast(0x80u + 37u * index + 11u * (index >> 4u)); + } + return bytes; +} + +std::uint32_t StoredComponent(const Format& format, std::uint32_t tid, std::uint32_t dmask, std::uint32_t component) { + if (((dmask >> component) & 1u) == 0u) return 0u; + std::uint32_t index = 0; + for (std::uint32_t lower = 0; lower < component; ++lower) index += (dmask >> lower) & 1u; + return Level(format, Value(format, tid, index)); +} + +std::vector Expected(const Format& format) { + auto bytes = Initial(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (const auto& write : StoreWrites) { + auto* texel = bytes.data() + TexelOffset(format, tid, write.row); + for (std::uint32_t component = 0; component < format.components; ++component) { + const auto value = StoredComponent(format, tid, write.dmask, component); + std::memcpy(texel + component * format.bits / 8u, &value, format.bits / 8u); + } + } + } + return bytes; +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::array TextureDescriptor(const void* data, std::uint32_t format, std::uint32_t swizzle) { + const auto address = static_cast(reinterpret_cast(data)); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (format << 20u) | (((Width - 1u) & 3u) << 30u), + ((Width - 1u) >> 2u) | ((Height - 1u) << 14u), + swizzle | (Type2D << 28u), + 0u, + 0u, + 0u, + 0u, + }; +} + +void Dispatch(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::span code, std::uint32_t format, std::uint32_t swizzle, const std::array& buffer) { + std::vector userData(16, 0u); + const auto texture = TextureDescriptor(texels, format, swizzle); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast(texels), TexelBytes, nullptr, "test"); + device.WaitIdle(); +} + +void Store(AgcDriver::VulkanDevice& device, std::uint8_t* texels, const Format& format) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t index = 0; index < 4u; ++index) Input[tid * 4u + index] = std::bit_cast(Value(format, tid, index)); + } + const auto initial = Initial(); + std::copy(initial.begin(), initial.end(), texels); + Dispatch(device, texels, StoreCode, format.format, SwizzleOf(format), BufferDescriptor(Input.data(), static_cast(Input.size() * 4u))); +} + +void CheckTexels(const std::uint8_t* texels, const Format& format) { + const auto expected = Expected(format); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto offset = TexelOffset(format, x, y); + for (std::uint32_t component = 0; component < format.components; ++component) { + std::uint32_t actual = 0; + std::uint32_t wanted = 0; + std::memcpy(&actual, texels + offset + component * format.bits / 8u, format.bits / 8u); + std::memcpy(&wanted, expected.data() + offset + component * format.bits / 8u, format.bits / 8u); + Require(actual == wanted, std::string("image_store of ") + format.name + ": texel (" + std::to_string(x) + ", " + std::to_string(y) + ") component " + std::to_string(component) + " is " + Hex(actual) + ", expected " + Hex(wanted) + " (level nearest to the exact product)"); + } + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + GuestBlock block; + auto* texels = block.Data(); + for (const auto& format : Formats) { + Store(*device, texels, format); + CheckTexels(texels, format); + } + std::puts("image store unorm rounding tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp index 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/ImageTableCache.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTableCache.cpp index 9a112123fe..2e2bf16ed9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTableCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTableCache.cpp @@ -131,7 +131,7 @@ Captured Capture(std::byte* table) { result.memory = std::make_unique(std::span{}); result.capture = result.memory->Capture(request); result.regions = result.memory->TakeRecentRegions(); - request.context.memory = result.memory->Regions(); + request.context.memory = result.regions; result.compiled = Recompile(request, *result.capture); return result; } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTableCacheBounds.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTableCacheBounds.cpp index 2c65e4d7a8..765947dc99 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTableCacheBounds.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTableCacheBounds.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/ShaderMemory.hpp" #include "Optimization/ResourceMaterializer.hpp" @@ -342,6 +343,32 @@ void SetBudgets() { #endif } +#ifdef _WIN32 +bool WindowsImportsStayWatchedThroughReads(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 host import probe failed at %s (%d)\n", probe.failure, static_cast(probe.result)); + } else { + std::printf( + "skipped, host imports on this device report %u of %u pages written at the import and " + "%u after a GPU read, so a watched imported table cannot be vouched for\n", + probe.writtenAtImport, probe.pages, probe.writtenAfterSubmit); + } + 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 + } int main(int argc, char** argv) { @@ -350,6 +377,12 @@ int main(int argc, char** argv) { { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; +#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; Prepare(block); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTableReuse.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTableReuse.cpp index d5a6b46847..a46aeb00be 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTableReuse.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTableReuse.cpp @@ -330,6 +330,20 @@ void RunTests(AgcDriver::VulkanDevice& device, const GuestBlock& block, const Gu Require(Dispatch(device, result, 15) == Row(block.Texels(15)), "image table reuse: the table after a dispatch wrote an entry sampled the wrong entry"); RunImageStore(device, stores); + + AgcDriver::Graphics::ReleaseTextureCaches(~std::uint64_t{0}); + Require(AgcDriver::Graphics::SampledTextureMemory() == 0u, "image table reuse: " + std::to_string(AgcDriver::Graphics::SampledTextureMemory()) + " bytes of sampled textures outlived a full release of the texture caches after the kept-table steps"); + + AgcDriver::GuestMemory::BumpCollectEpoch(); + Require(Dispatch(device, result, 29) == Row(block.Texels(29)), "image table reuse: the table before its textures were destroyed sampled the wrong entry"); + before = AgcDriver::Graphics::ImageTableReuses(); + Require(Dispatch(device, result, 29) == Row(block.Texels(29)), "image table reuse: the table built again before its textures were destroyed sampled the wrong entry"); + ExpectWrites(before, 1, 0, "a kept table built again before its textures were destroyed"); + AgcDriver::Graphics::ReleaseTextureCaches(~std::uint64_t{0}); + Require(AgcDriver::Graphics::SampledTextureMemory() == 0u, "image table reuse: " + std::to_string(AgcDriver::Graphics::SampledTextureMemory()) + " bytes of sampled textures outlived a full release of the texture caches after a table's set was kept"); + before = AgcDriver::Graphics::ImageTableReuses(); + Require(Dispatch(device, result, 29) == Row(block.Texels(29)), "image table reuse: a kept table whose textures were destroyed sampled the wrong entry"); + ExpectWrites(before, 0, 1, "a kept table whose textures were destroyed"); } } 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/IndirectThreadDimensions.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/IndirectThreadDimensions.cpp new file mode 100644 index 0000000000..37573087cf --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/IndirectThreadDimensions.cpp @@ -0,0 +1,214 @@ +#include "VulkanTestDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include +#include "prx/libSceAgcDriver/Submit/include/Acb.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +alignas(256) std::array AddressWriterCode{0x7e000202u, 0x7e020203u, 0x7e040204u, 0x7e060280u, 0xdc7c8000u, 0x00000003u, 0xbf810000u}; +alignas(256) std::array CounterCode{0xf4080200u, 0xfa000000u, 0xbf8cc07fu, 0x7e000281u, 0xe0c80000u, 0x80020000u, 0xbf810000u}; + +constexpr std::uint32_t ThreadDimensions = 0x8061u; +constexpr std::uint32_t GroupCounts = 0x8041u; +constexpr std::uint32_t Compute = 0x20u; +constexpr std::size_t GuestBytes = 1u << 20u; + +struct Slot { + alignas(256) std::array arguments{}; + alignas(256) std::array counter{}; + alignas(256) std::array counterDescriptor{}; + alignas(256) std::uint32_t done = 0; +}; + +struct Shape { + const char* name; + std::array threads; + std::array group; +}; + +constexpr std::array Shapes{{ + {"100x1x1 threads in groups of 64x1x1", {100u, 1u, 1u}, {64u, 1u, 1u}}, + {"5x3x2 threads in groups of 4x2x2", {5u, 3u, 2u}, {4u, 2u, 2u}}, + {"64x2x1 threads in groups of 32x1x1", {64u, 2u, 1u}, {32u, 1u, 1u}}, + {"0x7x1 threads in groups of 8x1x1", {0u, 7u, 1u}, {8u, 1u, 1u}}, +}}; + +std::uint32_t Low(const volatile void* pointer) { + return static_cast(reinterpret_cast(pointer)); +} + +std::uint32_t High(const volatile void* pointer) { + return static_cast(static_cast(reinterpret_cast(pointer)) >> 32u); +} + +std::array Descriptor(const void* pointer, std::uint32_t bytes) { + return {Low(pointer), High(pointer) & 0xffffu, bytes, 0x31016facu}; +} + +std::uint32_t Load(const std::uint32_t& value) { + return reinterpret_cast(value); +} + +std::uint32_t Expected(const Shape& shape) { + return shape.threads[0] * shape.threads[1] * shape.threads[2]; +} + +void SetShaderRegister(std::vector& words, std::uint32_t offset, std::uint32_t value) { + words.insert(words.end(), {0xc0017600u, offset, value}); +} + +void Program(std::vector& words, const void* code, std::array group, std::initializer_list userData) { + const auto address = static_cast(reinterpret_cast(code)); + SetShaderRegister(words, 0x20c, static_cast(address >> 8u)); + SetShaderRegister(words, 0x20d, static_cast(address >> 40u) & 0xffu); + for (std::uint32_t axis = 0; axis < 3; ++axis) SetShaderRegister(words, 0x207u + axis, group[axis]); + SetShaderRegister(words, 0x212, 0); + SetShaderRegister(words, 0x213, static_cast(userData.size()) << 1u); + std::uint32_t offset = 0x240; + for (const auto value : userData) SetShaderRegister(words, offset++, value); +} + +void Reset(Slot& slot) { + slot.arguments = {0xffffffffu, 0xffffffffu, 0xffffffffu, 0}; + slot.counter = {}; + slot.counterDescriptor = Descriptor(slot.counter.data(), sizeof(slot.counter)); + slot.done = 0; +} + +void Check(const Slot& slot, const Shape& shape, const std::string& name) { + const auto start = std::chrono::steady_clock::now(); + while (Load(slot.done) == 0) { + Require(std::chrono::steady_clock::now() - start < std::chrono::seconds(10), name + ": the completion label did not land"); + std::this_thread::sleep_for(std::chrono::microseconds(100)); + } + Require(Load(slot.counter[0]) == Expected(shape), name + ": " + std::to_string(Load(slot.counter[0])) + " threads ran, expected " + std::to_string(Expected(shape))); +} + +void RunSubmitted(Slot& slot, const Shape& shape, bool producedOnGpu) { + Reset(slot); + if (!producedOnGpu) { + for (std::size_t axis = 0; axis < 3; ++axis) slot.arguments[axis] = shape.threads[axis]; + } + std::vector words; + if (producedOnGpu) { + Program(words, AddressWriterCode.data(), {1u, 1u, 1u}, {Low(slot.arguments.data()), High(slot.arguments.data()), shape.threads[0], shape.threads[1], shape.threads[2]}); + words.insert(words.end(), {0xc0031500u, 1, 1, 1, GroupCounts}); + } + Program(words, CounterCode.data(), shape.group, {Low(slot.counterDescriptor.data()), High(slot.counterDescriptor.data())}); + words.insert(words.end(), {0xc0021102u, 1u, Low(slot.arguments.data()), High(slot.arguments.data())}); + words.insert(words.end(), {0xc0011600u, 0u, ThreadDimensions}); + words.insert(words.end(), {0xc0033700u, 0x00100200u, Low(&slot.done), High(&slot.done), 1u}); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + Require(sceAgcDriverSubmitAcb(Compute, &packet) == 0, "compute submit failed"); + AgcDriverWaitIdle_nid_postfix(); + Check(slot, shape, std::string(shape.name) + (producedOnGpu ? ", arguments stored by the GPU" : ", arguments stored by the CPU")); +} + +ShaderRecompiler::RecompileResult Recompile(AgcDriver::VulkanDevice& device, std::span code, std::span userData, std::span memory, std::array group, std::array partialThreads, std::uint32_t pushBytes) { + const ShaderRecompiler::ShaderComputeStageInfo compute{group, 0, {false, false, false}, false, 1, partialThreads}; + ShaderRecompiler::RecompileRequest request{ + {ShaderRecompiler::ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.ComputeTarget(32), + {0, 0, 0, pushBytes} + }; + request.useCache = false; + return ShaderRecompiler::Recompile(request); +} + +void RunCpuFallback(Slot& slot, const Shape& shape) { + AgcDriverShutdown_nid_postfix(); + Reset(slot); + const auto device = OpenVulkanTestDevice(); + Require(device != nullptr, "cannot open the CPU fallback test device"); + const std::array producerData{Low(slot.arguments.data()), High(slot.arguments.data()), shape.threads[0], shape.threads[1], shape.threads[2]}; + const std::array producerMemory{{{reinterpret_cast(AddressWriterCode.data()), std::as_bytes(std::span(AddressWriterCode))}}}; + const auto producer = Recompile(*device, AddressWriterCode, producerData, producerMemory, {1u, 1u, 1u}, {}, 128u); + const std::array consumerData{Low(slot.counterDescriptor.data()), High(slot.counterDescriptor.data())}; + const std::array consumerMemory{{ + {reinterpret_cast(CounterCode.data()), std::as_bytes(std::span(CounterCode))}, + {reinterpret_cast(slot.counterDescriptor.data()), std::as_bytes(std::span(slot.counterDescriptor))}, + }}; + const auto consumer = Recompile(*device, CounterCode, consumerData, consumerMemory, shape.group, {~0u, ~0u, ~0u}, 0u); + const auto preparedProducer = device->PrepareDispatch(producer, {}, 1, reinterpret_cast(AddressWriterCode.data())); + const auto preparedConsumer = device->PrepareDispatch(consumer, {}, 0, reinterpret_cast(CounterCode.data())); + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + auto* recorder = AgcDriver::Graphics::Recorder::Active(); + Require(recorder != nullptr, "the CPU fallback test has no active recorder"); + device->Dispatch(producer, 1, 1, 1, {}, reinterpret_cast(AddressWriterCode.data()), preparedProducer); + const auto labelAddress = reinterpret_cast(&slot.arguments[2]); + Require(device->WriteLabelOnGpu(labelAddress, std::as_bytes(std::span(&shape.threads[2], 1)), 1, Compute, false) == 0, "the argument label was not recorded on the GPU"); + const auto outcome = device->DispatchIndirect(consumer, reinterpret_cast(slot.arguments.data()), {}, reinterpret_cast(CounterCode.data()), preparedConsumer, nullptr, shape.group); + Require(outcome.cpuReason == 2, std::string(shape.name) + ": the indirect dispatch did not take the pending-label CPU fallback (reason " + std::to_string(outcome.cpuReason) + ")"); + recorder->Sync(); + slot.done = 1; + Check(slot, shape, std::string(shape.name) + ", read on the CPU behind a pending argument label"); +} + +} + +int main() { + try { + auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + device.reset(); +#ifdef _WIN32 + auto* block = static_cast(VirtualAlloc(nullptr, GuestBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + auto* block = static_cast(std::aligned_alloc(GuestBytes, GuestBytes)); +#endif + Require(block != nullptr, "cannot allocate guest memory"); + GuestAllocations::Mutation().Add(block, GuestBytes, true, true, true); + auto* slot = new (block) Slot{}; + const bool gpuThreadDimensions = std::getenv("APS5_NO_GPU_THREAD_DIMENSIONS") == nullptr; + for (const auto& shape : Shapes) { + if (gpuThreadDimensions) RunSubmitted(*slot, shape, true); + RunSubmitted(*slot, shape, false); + std::cout << shape.name << ": passed\n"; + } + if (gpuThreadDimensions) { + for (const auto& shape : Shapes) { + RunCpuFallback(*slot, shape); + std::cout << shape.name << ", CPU fallback: passed\n"; + } + } + std::cout << "indirect thread dimension tests passed\n"; + AgcDriverShutdown_nid_postfix(); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { + AgcDriverShutdown_nid_postfix(); + } catch (const std::exception& shutdownError) { + std::cerr << shutdownError.what() << '\n'; + } + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LayeredArrayColorDraw.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LayeredArrayColorDraw.cpp new file mode 100644 index 0000000000..8105de38e5 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LayeredArrayColorDraw.cpp @@ -0,0 +1,256 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Layered draws (a vertex shader exporting the render target index) into a color view of several +// slices: (a) slices 1 to 3 of a four-slice 2D array, (b) slices 0 to 2 of a surface of one slice. +// Each layer is filled with its own value; after the write-back every slice of guest memory must hold +// its layer's value, and the slices outside the view their initial bytes. + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Side = 128; +constexpr std::uint32_t Slices = 4; +constexpr std::size_t SliceBytes = 65536; +constexpr std::size_t SurfaceBytes = SliceBytes * Slices; +constexpr std::uint32_t TileR64KBX = 0x1b; +constexpr std::uint32_t Color32FloatInfo = (4u << 2u) | (7u << 8u); +constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u; +constexpr std::uint8_t Float32Export = 9; +constexpr std::uint32_t LayerExport = (1u << 18u) | (1u << 21u); +constexpr std::uint32_t ShaderViewportIndexLayerCapability = 5254; +constexpr std::uint32_t Kept = 0x5a5a5a5au; + +// Position from the buffer at offset 0, the layer from offset 28 into the render target index. +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xe030201c, 0x80000405, 0xbf8c3f70, 0xf80000cf, 0x03020100, 0xf80008d4, 0x00040000, 0xbf810000, +}; + +// 1 + layer / 2, the layer read from the render target index field of the ancillary word. +alignas(256) constexpr std::array LayerValuePixelCode{ + 0x2c020090, 0x7e020d01, 0x100202f0, 0x060202f2, 0xf800180f, 0x01010101, 0xbf810000, +}; + +struct Vertex { + std::array position; + std::array color; + std::uint32_t layer; +}; + +alignas(256) std::array Vertices{}; + +float LayerValue(std::uint32_t layer) { + return 1.0f + static_cast(layer) / 2.0f; +} + +std::uint64_t AddressOf(const void* data) { + return static_cast(reinterpret_cast(data)); +} + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(SurfaceBytes, SliceBytes)); + if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, SurfaceBytes, PAGE_READWRITE, SurfaceBytes); +#else + void* mapped = mmap(nullptr, SurfaceBytes + SliceBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapped != MAP_FAILED) { + const auto begin = reinterpret_cast(mapped); + const auto aligned = (begin + SliceBytes - 1) & ~(static_cast(SliceBytes) - 1); + if (aligned != begin) munmap(mapped, aligned - begin); + if (aligned + SurfaceBytes != begin + SurfaceBytes + SliceBytes) munmap(reinterpret_cast(aligned + SurfaceBytes), begin + SliceBytes - aligned); + block = reinterpret_cast(aligned); + if (AgcDriver::GuestMemory::WriteWatched()) GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, SurfaceBytes); + } +#endif + Require(block != nullptr, "layered array color draw: cannot allocate the color array"); + GuestAllocations::Mutation().Add(block, SurfaceBytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(block, SurfaceBytes); + GuestArena::GuestArenaRelease_nid_postfix(block, SurfaceBytes); +#else + munmap(block, SurfaceBytes); + if (AgcDriver::GuestMemory::WriteWatched()) GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, SurfaceBytes); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; +}; + +void BuildQuads(std::uint32_t layers) { + Vertices = {}; + const std::array, 6> corners{{{-1.0f, -1.0f}, {1.0f, -1.0f}, {-1.0f, 1.0f}, {1.0f, -1.0f}, {1.0f, 1.0f}, {-1.0f, 1.0f}}}; + for (std::uint32_t layer = 0; layer < layers; ++layer) { + for (std::uint32_t k = 0; k < corners.size(); ++k) Vertices[6u * layer + k] = {{corners[k][0], corners[k][1], 0.5f, 1.0f}, {1.0f, 1.0f, 1.0f}, layer}; + } +} + +// A CB_COLOR_VIEW of slices first..first + layers - 1 of a surface whose last array slice is lastArray. +AgcDriver::Graphics::ColorTarget DecodeColor(std::uint64_t address, std::uint32_t lastArray, std::uint32_t first, std::uint32_t layers) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(address >> 8u); + cx[0x31b] = first | ((first + layers - 1u) << 13u); + cx[0x31c] = Color32FloatInfo; + cx[0x31d] = 0; + cx[0x3b0] = ((Side - 1u) << 14u) | (Side - 1u); + cx[0x3b8] = (TiledAttrib3 & ~((0x1fu << 14u) | 0x1fffu)) | (TileR64KBX << 14u) | lastArray; + cx[0x390] = static_cast(address >> 40u); + cx[0x207] = LayerExport; + const auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + const auto arrayLast = std::max(lastArray, first + layers - 1u); + Require(color.layers == layers && color.arraySlice == first && color.arrayLastSlice == arrayLast && color.arrayAddress == address, "the color view did not decode as " + std::to_string(layers) + " layers from slice " + std::to_string(first) + " of an array whose last slice is " + std::to_string(arrayLast)); + return color; +} + +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color) { + const auto target = device.Target(); + constexpr std::uint32_t waveSize = 64; + const auto vertexCount = 6u * color.layers; + BuildQuads(color.layers); + std::vector vertexUserData(4, 0u); + const auto address = AddressOf(Vertices.data()); + const std::array vertexBuffer{static_cast(address), static_cast((address >> 32u) & 0xffffu) | (static_cast(sizeof(Vertex)) << 16u), vertexCount, 0x01016facu}; + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + ShaderRecompiler::ShaderVertexStageInfo vertexInfo{}; + vertexInfo.paClVsOutCntl = LayerExport; + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, vertexInfo, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + VkPipelineColorBlendAttachmentState blend{}; + blend.colorWriteMask = 0xf; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.colors = {color}; + state.color = color; + state.hasColorTarget = true; + state.renderExtent = {Side, Side}; + state.renderLayers = color.layers; + state.layerExports = LayerExport; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Side), static_cast(Side), -static_cast(Side), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Side, Side}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blends = {blend}; + state.blend = blend; + state.blendConstants = {}; + + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.targetOutputMode[0] = Float32Export; + pixel.targetExportMapping = AgcDriver::Graphics::ExportMappings(state); + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary); + pixel.ancillary = true; + pixel.layerExported = true; + const std::span pixelCode(LayerValuePixelCode); + const auto codeAddress = reinterpret_cast(pixelCode.data()); + const std::array pixelMemory{{{codeAddress, std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, codeAddress, pixelCode, 0, {}}, + {waveSize, 0, {}, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + const AgcDriver::Pm4::DrawParameters draw{0, vertexCount, 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +// Draws into slices first..first + layers - 1 and checks every slice of the four in guest memory. +void Run(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t lastArray, std::uint32_t first, std::uint32_t layers, const std::string& what) { + const auto surface = AddressOf(texels); + std::fill_n(reinterpret_cast(texels), SurfaceBytes / 4u, Kept); + AgcDriver::GuestMemory::CollectWrites(surface, SurfaceBytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + Draw(device, DecodeColor(surface, lastArray, first, layers)); + AgcDriver::Graphics::StorageTexture::FlushPending(surface, SurfaceBytes, nullptr, "layered array color draw test"); + device.WaitIdle(); + for (std::uint32_t slice = 0; slice < Slices; ++slice) { + const bool drawn = slice >= first && slice < first + layers; + const auto expected = drawn ? std::bit_cast(LayerValue(slice - first)) : Kept; + const auto* words = reinterpret_cast(texels + slice * SliceBytes); + for (std::size_t word = 0; word < SliceBytes / 4u; ++word) { + if (words[word] == expected) continue; + char actual[16]; + std::snprintf(actual, sizeof(actual), "0x%08x", words[word]); + Require(false, what + ": slice " + std::to_string(slice) + " word " + std::to_string(word) + " is " + actual + ", expected " + (drawn ? std::to_string(LayerValue(slice - first)) + " (layer " + std::to_string(slice - first) + ")" : "its initial bytes (outside the color view)")); + } + } +} + +} + +int main() { + try { + GuestBlock array; + GuestBlock single; + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto target = device->Target(); + if (std::find(target.supportedCapabilities.begin(), target.supportedCapabilities.end(), ShaderViewportIndexLayerCapability) == target.supportedCapabilities.end()) { + std::puts("skipped, the device has no VK_EXT_shader_viewport_index_layer"); + return VulkanTestSkipped; + } + Run(*device, array.Data(), Slices - 1u, 1, 3, "a layered draw into slices 1 to 3 of a four-slice 2D array"); + Run(*device, single.Data(), 0, 0, 3, "a layered draw into slices 0 to 2 of a surface of one slice"); + std::puts("layered array color draw tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LdsTreeReduction.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LdsTreeReduction.cpp new file mode 100644 index 0000000000..5188455a21 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LdsTreeReduction.cpp @@ -0,0 +1,84 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 256; +constexpr std::uint32_t Rounds = 16; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array TreeCode{ + 0x34040082, 0x4a020081, 0xd8340000, 0x00000102, 0xbf8cc07f, 0xbf8a0000, 0x7da800ff, 0x00000080, + 0xbf880006, 0xd8d80200, 0x03000002, 0xbf8cc07f, 0x4a020701, 0xd8340000, 0x00000102, 0xbefe03c1, + 0xbf8cc07f, 0xbf8a0000, 0x7da800c0, 0xbf880006, 0xd8d80100, 0x03000002, 0xbf8cc07f, 0x4a020701, + 0xd8340000, 0x00000102, 0xbefe03c1, 0xbf8cc07f, 0xbf8a0000, 0x7da800a0, 0xbf880006, 0xd8d80080, + 0x03000002, 0xbf8cc07f, 0x4a020701, 0xd8340000, 0x00000102, 0xbefe03c1, 0xbf8cc07f, 0xbf8a0000, + 0x7da80090, 0xbf880006, 0xd8d80040, 0x03000002, 0xbf8cc07f, 0x4a020701, 0xd8340000, 0x00000102, + 0xbefe03c1, 0xbf8cc07f, 0x7da80088, 0xbf880006, 0xd8d80020, 0x03000002, 0xbf8cc07f, 0x4a020701, + 0xd8340000, 0x00000102, 0xbefe03c1, 0xbf8cc07f, 0x7da80084, 0xbf880006, 0xd8d80010, 0x03000002, + 0xbf8cc07f, 0x4a020701, 0xd8340000, 0x00000102, 0xbefe03c1, 0xbf8cc07f, 0x7da80082, 0xbf880006, + 0xd8d80008, 0x03000002, 0xbf8cc07f, 0x4a020701, 0xd8340000, 0x00000102, 0xbefe03c1, 0xbf8cc07f, + 0x7da80081, 0xbf880006, 0xd8d80004, 0x03000002, 0xbf8cc07f, 0x4a020701, 0xd8340000, 0x00000102, + 0xbefe03c1, 0xbf8cc07f, 0xbf8a0000, 0x7e060280, 0xd8d80000, 0x03000003, 0xbf8cc07f, 0xe0701000, + 0x80000302, 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}; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto target = device->ComputeTarget(32); + if (target.subgroupSize != 32u) { + std::printf("skipped, wave32 programs do not run on 32-lane subgroups (%u lanes)\n", target.subgroupSize); + return VulkanTestSkipped; + } + std::vector userData(4, 0u); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(output.begin(), output.end(), userData.begin()); + const std::span code(TreeCode); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, Threads, {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}, + target, + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + constexpr std::uint32_t Total = Threads * (Threads + 1u) / 2u; + for (std::uint32_t round = 0; round < Rounds; ++round) { + Output.fill(0xdeadbeefu); + device->Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device->WaitIdle(); + for (std::uint32_t thread = 0; thread < Threads; ++thread) { + Require(Output[thread] == Total, "LDS tree reduction over eight wave32 waves, round " + std::to_string(round) + ": thread " + std::to_string(thread) + " (wave " + std::to_string(thread / 32u) + ") read " + std::to_string(Output[thread]) + ", expected " + std::to_string(Total)); + } + } + std::puts("LDS tree reduction tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/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/LookupLogFitsBuild.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LookupLogFitsBuild.cpp new file mode 100644 index 0000000000..5ca84cfaf6 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LookupLogFitsBuild.cpp @@ -0,0 +1,214 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using AgcDriver::Graphics::CompiledShader; +using AgcDriver::Graphics::Require; +using AgcDriver::Graphics::ShaderResources; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Lanes = 64; +constexpr std::uint32_t Textures = 1500; +constexpr std::size_t LogBound = 1024; +constexpr std::uint32_t Stride = 32; +constexpr std::size_t SurfaceBytes = 256; +constexpr std::uint32_t Format8888UNorm = 56; +constexpr std::uint32_t Type2D = 9; +constexpr std::size_t SrtOffset = 4096; +constexpr std::size_t InputOffset = 8192; +constexpr std::size_t OutputOffset = 12288; +constexpr std::size_t HeapOffset = 65536; +constexpr std::size_t BlockBytes = (HeapOffset + std::size_t{Textures} * Stride + 65535u) & ~std::size_t{65535}; +constexpr std::size_t SurfaceBlockBytes = (std::size_t{Textures} * SurfaceBytes + 65535u) & ~std::size_t{65535}; + +constexpr std::array Code{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, + 0x34080082u, 0x7e200500u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xbf8cc07fu, + 0xe0301000u, 0x80030504u, 0xf09c0f08u, 0x00450000u, 0xbf8c3f70u, 0x4a000105u, + 0xe0701000u, 0x80070004u, 0xbf810000u, 0u, 0u}; + +class GuestBlock { +public: + GuestBlock(std::size_t bytes, bool gpu) { +#ifdef _WIN32 + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, 65536)); + if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, bytes, PAGE_READWRITE, bytes); +#else + constexpr std::uintptr_t alignment = 65536; + void* mapped = mmap(nullptr, bytes + alignment, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapped != MAP_FAILED) { + const auto begin = reinterpret_cast(mapped); + const auto aligned = (begin + alignment - 1) & ~(alignment - 1); + if (aligned != begin) munmap(mapped, aligned - begin); + if (aligned + bytes != begin + bytes + alignment) munmap(reinterpret_cast(aligned + bytes), begin + alignment - aligned); + block = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, bytes); + } +#endif + Require(block != nullptr, "lookup log: cannot allocate a guest block"); + std::memset(block, 0, bytes); + GuestAllocations::Mutation().Add(block, bytes, true, true, gpu); + AgcDriver::GuestMemory::CollectWritesUncached(reinterpret_cast(block), bytes); + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint64_t At(std::size_t offset) const { return reinterpret_cast(block + offset); } + std::uint8_t* Bytes(std::size_t offset) const { return block + offset; } + std::uint32_t* Words(std::size_t offset) const { return reinterpret_cast(block + offset); } + +private: + std::uint8_t* block = nullptr; +}; + +std::array BufferDescriptor(std::uint64_t address, std::uint32_t stride, std::uint32_t records) { + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), records, stride == 0u ? 0x31016facu : 0x01016facu}; +} + +void Prepare(const GuestBlock& block, const GuestBlock& surfaces) { + std::memcpy(block.Words(0), Code.data(), sizeof(Code)); + for (std::uint32_t texture = 0; texture < Textures; ++texture) { + const auto offset = static_cast(texture) * SurfaceBytes; + std::memset(surfaces.Bytes(offset), static_cast(texture & 0xffu), SurfaceBytes); + const auto surface = surfaces.At(offset); + const std::array words{static_cast(surface >> 8u), static_cast((surface >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (3u << 30u), 1u | (7u << 14u), 0xfacu | (Type2D << 28u), 0u, 0u, 0u, 0u}; + std::memcpy(block.Words(HeapOffset + static_cast(texture) * Stride), words.data(), sizeof(words)); + } + auto* srt = block.Words(SrtOffset); + const auto heap = BufferDescriptor(block.At(HeapOffset), Stride, Textures); + const auto input = BufferDescriptor(block.At(InputOffset), 0u, Lanes * 4u); + const auto output = BufferDescriptor(block.At(OutputOffset), 0u, Lanes * 4u); + std::copy(heap.begin(), heap.end(), srt); + std::copy(input.begin(), input.end(), srt + 8); + std::copy(output.begin(), output.end(), srt + 12); + AgcDriver::GuestMemory::CollectWritesUncached(block.At(0), BlockBytes); + AgcDriver::GuestMemory::CollectWritesUncached(surfaces.At(0), SurfaceBlockBytes); +} + +ShaderRecompiler::RecompileResult Compile(const AgcDriver::VulkanDevice& device, const GuestBlock& block) { + const auto srt = block.At(SrtOffset); + const std::array userData{static_cast(srt), static_cast(srt >> 32u)}; + const std::span code(block.Words(0), Code.size()); + const std::array memory{{ + {block.At(0), std::as_bytes(code)}, + {srt, std::as_bytes(std::span(block.Words(SrtOffset), 64))}, + {block.At(HeapOffset), std::as_bytes(std::span(block.Words(HeapOffset), static_cast(Textures) * Stride / 4u))}, + }}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Lanes, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, block.At(0), code, 0, {}}, + {Lanes, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + return ShaderRecompiler::Recompile(request); +} + +ShaderResources::ProofReport Prove(ShaderResources& resources, const CompiledShader& shader, bool& current) { + ShaderResources::ProofReport report; + current = resources.Revalidate(shader, &report); + return report; +} + +std::string Path(const ShaderResources::ProofReport& report) { + switch (report.path) { + case ShaderResources::ProofPath::Fast: return "fast"; + case ShaderResources::ProofPath::OwnRefreshed: return "own refresh"; + default: return "full walk"; + } +} + +void RunTests(const AgcDriver::VulkanDevice& device) { + GuestBlock block(BlockBytes, true); + GuestBlock surfaces(SurfaceBlockBytes, false); + Prepare(block, surfaces); + const auto result = Compile(device, block); + const auto table = std::find_if(result.bindings.begin(), result.bindings.end(), [](const auto& binding) { return binding.role == ShaderRecompiler::DescriptorRole::ImageTable; }); + Require(table != result.bindings.end() && table->count == Textures, "lookup log: the program does not bind an image table of " + std::to_string(Textures) + " textures"); + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + const CompiledShader shader{ShaderStage::Compute, &result, 0}; + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + auto resources = std::make_shared(context, shader); + Require(resources->Reusable() && resources->SampledTextures().size() == Textures, "lookup log: the set over " + std::to_string(Textures) + " textures is not reusable"); + const auto capacity = ShaderResources::LookupLogCapacity(); + Require(capacity <= LogBound, "lookup log: the thread's log kept a capacity of " + std::to_string(capacity) + " records after a build of " + std::to_string(Textures) + " textures, over the bound of " + std::to_string(LogBound)); + std::printf("the thread's log capacity is %zu records after the build\n", capacity); + bool current = false; + auto report = Prove(*resources, shader, current); + Require(current && report.path == ShaderResources::ProofPath::Fast, "lookup log: a set over " + std::to_string(Textures) + " textures took the " + Path(report) + " with its memory unchanged after the build"); + std::puts("unchanged memory proved on the fast path after the build"); + const auto target = static_cast(Textures / 3u) * SurfaceBytes; + auto* first = reinterpret_cast(surfaces.Bytes(target)); + const std::uint8_t saved = *first; + *first = saved; + report = Prove(*resources, shader, current); + Require(report.path == ShaderResources::ProofPath::Full, "lookup log: a store over texture " + std::to_string(Textures / 3u) + " took the " + Path(report) + ", not the full walk"); + Require(ShaderResources::LookupLogCapacity() <= LogBound, "lookup log: the thread's log kept a capacity of " + std::to_string(ShaderResources::LookupLogCapacity()) + " records after a full walk over " + std::to_string(Textures) + " textures"); + if (current) { + report = Prove(*resources, shader, current); + Require(current && report.path == ShaderResources::ProofPath::Fast, "lookup log: a set over " + std::to_string(Textures) + " textures took the " + Path(report) + " with its memory unchanged after a full walk"); + std::puts("unchanged memory proved on the fast path after a full walk"); + } + *first = static_cast(saved ^ 0xffu); + report = Prove(*resources, shader, current); + Require(report.path == ShaderResources::ProofPath::Full && !current, "lookup log: a set whose texture " + std::to_string(Textures / 3u) + " changed was proved current on the " + Path(report)); + std::puts("a changed texture fails the proof"); +} + +} + +int main() { +#ifdef _WIN32 + if (std::getenv("APS5_TEMPLATE_AUDIT_MS") == nullptr) _putenv_s("APS5_TEMPLATE_AUDIT_MS", "0"); +#else + setenv("APS5_TEMPLATE_AUDIT_MS", "0", 0); +#endif + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + RunTests(*device); + std::puts("lookup log fits build tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/MeshBarycentricEmulation.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MeshBarycentricEmulation.cpp new file mode 100644 index 0000000000..58f20d44f9 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MeshBarycentricEmulation.cpp @@ -0,0 +1,306 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::PixelInput; +using ShaderRecompiler::PixelInputBit; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 48; +constexpr float Unwritten = -7.0f; +alignas(256) std::array Pixels{}; + +alignas(256) constexpr std::array MeshCode{ + 0x8f6a9003, 0x94fe6ac1, 0xbf88000b, 0xd7650006, 0x000100c1, 0xd7660006, 0x00020cc1, 0x93ebff03, + 0x00040018, 0xd7460006, 0x04190c6b, 0x340c0c82, 0xd8340000, 0x00000506, 0xbf8cc07f, 0xbefe04c1, + 0xbf8a0000, 0x938dff02, 0x00090016, 0x9382ff03, 0x00040018, 0x938cff03, 0x00080008, 0xd7650009, + 0x000100c1, 0xd7660009, 0x000212c1, 0xd746000a, 0x04250c02, 0x7da8120c, 0xbf88002e, 0x361600ff, + 0x0000ffff, 0x2c180090, 0x361a02ff, 0x0000ffff, 0xd8d80000, 0x0b00000b, 0xd8d80000, 0x0c00000c, + 0xd8d80000, 0x0d00000d, 0xbf8cc07f, 0xe0382000, 0x8002100b, 0xe0382010, 0x8002140b, 0xe0382000, + 0x8002180c, 0xe0382010, 0x80021c0c, 0xe0382000, 0x8002200d, 0xe0382010, 0x8002240d, 0x161c1483, + 0x161e14ff, 0x00000060, 0xbf8c3f70, 0xdb7c0400, 0x0000100f, 0xdb7c0410, 0x0000140f, 0xdb7c0420, + 0x0000180f, 0xdb7c0430, 0x00001c0f, 0xdb7c0440, 0x0000200f, 0xdb7c0450, 0x0000240f, 0x4a501c81, + 0x4a521c82, 0x3450508a, 0x34525294, 0xd772002a, 0x04a6510e, 0xbf8cc07f, 0xbefe04c1, 0xbf8a0000, + 0x930e830d, 0xbf078002, 0xbf850003, 0x8f0f8c0d, 0x887c0f0e, 0xbf900009, 0x7da8140d, 0xbf880002, + 0xf8000941, 0x0000002a, 0xbefe04c1, 0x7da8140e, 0xbf88000a, 0x34561485, 0xdbfc0400, 0x2c00002b, + 0xdbfc0410, 0x3000002b, 0xbf8cc07f, 0xf80008cf, 0x2f2e2d2c, 0xf800020f, 0x33323130, 0xbf810000, +}; + +alignas(256) constexpr std::array BarycentricPixelCode{ + 0xf800180f, 0x01000100, 0xbf810000, +}; + +alignas(256) constexpr std::array VertexValuePixelCode{ + 0xc80a0000, 0xc80e0001, 0xc8120002, 0xf800180f, 0x04040302, 0xbf810000, +}; + +struct Vertex { + std::array position; + std::array color; +}; + +struct Corner { + float x; + float y; + float w; +}; + +constexpr std::array VertexValues{0.25f, -3.5f, 7.75f}; + +constexpr std::array Corners{{{-0.8f, -0.8f, 1.0f}, {0.9f, -0.6f, 2.5f}, {-0.5f, 0.85f, 0.5f}}}; + +alignas(256) std::array Vertices{}; + +std::array VertexBufferDescriptor(const void* vertices, std::uint32_t count) { + const auto address = reinterpret_cast(vertices); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (32u << 16u), count, 0x01016facu}; +} + +constexpr ShaderRecompiler::MeshConfiguration SmallSubgroup{4u, 4u, 12u, 12u, 4u, 64u, 1024u, 0u, 4u}; + +template +void ExpectRejected(TAction action, const std::string& reason) { + try { + action(); + } catch (const std::exception& error) { + Require(std::string_view(error.what()).find(reason) != std::string_view::npos, std::string("the validator rejected the draw for another reason than \"") + reason + "\": " + error.what()); + return; + } + Require(false, "the validator accepted a draw it must reject (" + reason + ")"); +} + +std::uint32_t DecorationWord(const ShaderRecompiler::RecompileResult& result, spv::Decoration decoration) { + const auto& words = result.spirv.Words(); + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> 16u; + if ((words[cursor] & 0xffffu) == spv::OpDecorate && count == 3u && words[cursor + 2] == decoration) return static_cast(cursor + 2); + cursor += count; + } + return 0u; +} + +void CheckValidator(AgcDriver::VulkanDevice& device, const AgcDriver::Pm4::DrawParameters& draw, const AgcDriver::Graphics::State& state, const ShaderRecompiler::RecompileResult& meshResult, const ShaderRecompiler::RecompileResult& pixelResult, std::uint32_t pixelOffset) { + const auto rejected = [&](const ShaderRecompiler::RecompileResult& mesh, const ShaderRecompiler::RecompileResult& pixel, const std::string& reason) { + const std::array shaders{{{ShaderStage::Mesh, &mesh, 0}, {ShaderStage::Fragment, &pixel, pixelOffset}}}; + ExpectRejected([&] { device.Draw(state, draw, shaders); }, reason); + }; + auto unreserved = pixelResult; + unreserved.barycentricEmulation.active = false; + rejected(meshResult, unreserved, "per-primitive"); + auto shifted = pixelResult; + shifted.fragmentParameters.push_back({0u, 0u, false, false}); + rejected(meshResult, shifted, "per-primitive"); + auto plain = pixelResult; + const auto decorate = DecorationWord(plain, spv::DecorationPerPrimitiveEXT); + Require(decorate != 0u, "the pixel module has no per-primitive input"); + plain.spirv[decorate] = spv::DecorationFlat; + rejected(meshResult, plain, "mesh shading"); +} + +ShaderRecompiler::RecompileResult Draw(AgcDriver::VulkanDevice& device, std::span pixelCode, std::uint32_t inputs) { + Pixels.fill(Unwritten); + auto target = device.Target(); + target.fragmentShaderBarycentricEnabled = false; + + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Vertices.size()), 0, 1, 0, false}; + std::vector meshUserData(12, 0u); + const auto index = AgcDriver::Graphics::MeshIndexBufferDescriptor(draw); + std::copy(index.begin(), index.end(), meshUserData.begin() + ShaderRecompiler::MeshIndexBufferUserWord); + const auto vertexBuffer = VertexBufferDescriptor(Vertices.data(), static_cast(Vertices.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), meshUserData.begin() + 8); + const std::array meshMemory{{{reinterpret_cast(MeshCode.data()), std::as_bytes(std::span(MeshCode))}}}; + ShaderRecompiler::RecompileRequest mesh{ + {ShaderStage::Mesh, reinterpret_cast(MeshCode.data()), MeshCode, 0, {}}, + {64u, 0, meshUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, meshMemory}, + target, + {0, 0, 0, ShaderRecompiler::MeshDrawPushOffsetBytes}, + ShaderRecompiler::GraphicsCompileContext{0, {}, SmallSubgroup, std::nullopt, {draw.indexAddress, draw.indexCount, draw.indexSize, draw.instanceCount}} + }; + mesh.useCache = false; + const auto meshResult = ShaderRecompiler::Recompile(mesh); + const auto meshPush = static_cast(meshResult.pushConstants.size()); + + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.interpolatorCount = 1; + pixel.inputAddr = inputs; + pixel.hasPerspectiveCenterVgpr = (inputs & PixelInputBit(PixelInput::PerspectiveCenter)) != 0u; + pixel.noPerspective = (inputs & PixelInputBit(PixelInput::LinearCenter)) != 0u; + pixel.executeOnNoop = true; + pixel.targetOutputMode[0] = 9; + pixel.targetExportMapping.fill(0xe4u); + const std::vector pixelUserData; + const std::array pixelMemory{{{reinterpret_cast(pixelCode.data()), std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, + {64u, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(meshPush, target), + ShaderRecompiler::GraphicsCompileContext{0, {}, SmallSubgroup, std::nullopt, {draw.indexAddress, draw.indexCount, draw.indexSize, draw.instanceCount}} + }; + fragment.useCache = false; + auto pixelResult = ShaderRecompiler::Recompile(fragment); + Require(pixelResult.barycentricEmulation.active, "the pixel shader did not emulate barycentrics"); + mesh.graphics->meshBarycentrics = ShaderRecompiler::MeshBarycentricLayoutOf(pixelResult); + const auto pairedMesh = ShaderRecompiler::Recompile(mesh); + if (pixelCode.data() == BarycentricPixelCode.data()) { + auto last = mesh; + last.graphics->mesh->provokingVertex = 1u; + ExpectRejected([&] { static_cast(ShaderRecompiler::Recompile(last)); }, "first provoking vertex"); + auto wide = mesh; + wide.graphics->mesh->maxVertices = target.mesh->maxSharedMemoryBytes / 16u + 1u; + ExpectRejected([&] { static_cast(ShaderRecompiler::Recompile(wide)); }, "mesh shared memory"); + } + Require(pairedMesh.meshBarycentrics == mesh.graphics->meshBarycentrics, "the paired mesh does not record the layout it was compiled for"); + Require(pairedMesh.pushConstants.size() == meshPush && pairedMesh.parameterExports == meshResult.parameterExports, "the paired mesh changed the push constants or exports the pixel shader was built for"); + + const std::array shaders{{ + {ShaderStage::Mesh, &pairedMesh, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(meshPush, target)} + }}; + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Geometry, 0x20u, 64u, 64u, SmallSubgroup, std::nullopt}; + state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R32G32B32A32_SFLOAT, sizeof(Pixels), 0xe4u, AgcDriver::Graphics::ColorTileMode::Linear, 16u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + if (pixelCode.data() == BarycentricPixelCode.data()) CheckValidator(device, draw, state, pairedMesh, pixelResult, PixelPushOffset(meshPush, target)); + device.Draw(state, draw, shaders); + device.WaitIdle(); + return pixelResult; +} + +struct Expected { + bool inside; + std::array linear; + std::array perspective; +}; + +Expected Barycentrics(std::uint32_t x, std::uint32_t y, bool snapped) { + std::array, 3> window{}; + for (std::size_t i = 0; i < Corners.size(); ++i) window[i] = {(Corners[i].x + 1.0) * Width / 2.0, (1.0 - Corners[i].y) * Height / 2.0}; + if (snapped) { + for (auto& corner : window) { + for (auto& coordinate : corner) coordinate = std::round(coordinate * 256.0) / 256.0; + } + } + const double px = x + 0.5; + const double py = y + 0.5; + const auto edge = [](const std::array& a, const std::array& b, double cx, double cy) { return (b[0] - a[0]) * (cy - a[1]) - (b[1] - a[1]) * (cx - a[0]); }; + const double area = edge(window[0], window[1], window[2][0], window[2][1]); + const std::array b{edge(window[1], window[2], px, py) / area, edge(window[2], window[0], px, py) / area, edge(window[0], window[1], px, py) / area}; + Expected expected{std::min({b[0], b[1], b[2]}) > 0.02, {}, {}}; + double sum = 0.0; + for (std::size_t i = 0; i < 3; ++i) sum += b[i] / Corners[i].w; + for (std::size_t i = 0; i < 3; ++i) { + expected.linear[i] = static_cast(b[i]); + expected.perspective[i] = static_cast(b[i] / Corners[i].w / sum); + } + return expected; +} + +bool MatchesEither(float value, float unsnapped, float snapped) { + return std::fabs(value - unsnapped) <= 1e-4f || std::fabs(value - snapped) <= 1e-4f; +} + +float Pixel(std::uint32_t x, std::uint32_t y, std::uint32_t channel) { + return Pixels[(y * Width + x) * 4u + channel]; +} + +std::uint32_t CheckVertexValues() { + const std::array expected{VertexValues[1] - VertexValues[0], VertexValues[2] - VertexValues[0], VertexValues[0]}; + std::uint32_t checked = 0; + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + if (!Barycentrics(x, y, false).inside) continue; + for (std::uint32_t channel = 0; channel < 3; ++channel) { + Require(Pixel(x, y, channel) == expected[channel], "v_interp_mov: pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") channel " + std::to_string(channel) + " is " + std::to_string(Pixel(x, y, channel)) + ", expected " + std::to_string(expected[channel])); + } + ++checked; + } + } + Require(checked > 200u, "v_interp_mov: too few interior pixels were checked"); + return checked; +} + +std::uint32_t CheckBarycentrics(const std::string& what, bool perspective) { + std::uint32_t checked = 0; + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto expected = Barycentrics(x, y, false); + if (!expected.inside) continue; + const auto snapped = Barycentrics(x, y, true); + const auto& weights = perspective ? expected.perspective : expected.linear; + const auto& snappedWeights = perspective ? snapped.perspective : snapped.linear; + for (std::uint32_t channel = 0; channel < 4; ++channel) { + const auto value = Pixel(x, y, channel); + const auto reference = weights[1u + (channel & 1u)]; + const auto snappedReference = snappedWeights[1u + (channel & 1u)]; + Require(MatchesEither(value, reference, snappedReference), what + ": pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") channel " + std::to_string(channel) + " is " + std::to_string(value) + ", expected " + std::to_string(reference) + " or " + std::to_string(snappedReference)); + } + ++checked; + } + } + Require(checked > 200u, what + ": too few interior pixels were checked"); + return checked; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!device->Target().mesh.has_value()) { + std::puts("skipped, the device has no VK_EXT_mesh_shader"); + return VulkanTestSkipped; + } + if (device->DeviceName().starts_with("llvmpipe")) { + std::puts("skipped, the device has no mesh shader compiler that handles these programs (llvmpipe)"); + return VulkanTestSkipped; + } + for (std::size_t i = 0; i < Corners.size(); ++i) { + const auto& corner = Corners[i]; + Vertices[i] = {{corner.x * corner.w, corner.y * corner.w, 0.5f * corner.w, corner.w}, {VertexValues[i], 0.0f, 0.0f, 1.0f}}; + } + const auto perspective = PixelInputBit(PixelInput::PerspectiveCenter); + const auto linear = PixelInputBit(PixelInput::LinearCenter); + auto result = Draw(*device, BarycentricPixelCode, perspective); + Require(result.barycentricEmulation.smooth && !result.barycentricEmulation.linear, "reading PERSP_CENTER I/J did not emulate perspective barycentrics"); + const auto smooth = CheckBarycentrics("mesh PERSP_CENTER I/J", true); + result = Draw(*device, BarycentricPixelCode, linear); + Require(!result.barycentricEmulation.smooth && result.barycentricEmulation.linear, "reading LINEAR_CENTER I/J did not emulate linear barycentrics"); + const auto noPerspective = CheckBarycentrics("mesh LINEAR_CENTER I/J", false); + result = Draw(*device, VertexValuePixelCode, perspective); + Require(result.barycentricEmulation.active && result.fragmentParameters.size() == 1 && result.fragmentParameters[0].perVertex, "v_interp_mov of P10 and P20 did not emulate per-vertex parameters"); + const auto vertexValues = CheckVertexValues(); + std::printf("mesh barycentric emulation tests passed: %u perspective and %u linear I/J pixels, %u raw vertex value pixels\n", smooth, noPerspective, vertexValues); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleRetirement.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleRetirement.cpp index 4773f3451f..b603cc7326 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleRetirement.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleRetirement.cpp @@ -407,6 +407,51 @@ public: AwaitImages(0, "sampled MSAA targets were not fully released"); } + void OrderRemap() { + const auto locations = context.sampleLocationSampleCounts; + const auto grid = context.sampleLocationGridSampleCounts; + context.sampleLocationSampleCounts |= VK_SAMPLE_COUNT_4_BIT; + context.sampleLocationGridSampleCounts &= ~static_cast(VK_SAMPLE_COUNT_4_BIT); + GuestBlock block; + const auto target = Target(block); + if (!MayReorderSamples(context, target.samples) || SampleOrderStorageFormat(context, target.format, target.elementBytes, target.samples) == VK_FORMAT_UNDEFINED) { + context.sampleLocationSampleCounts = locations; + context.sampleLocationGridSampleCounts = grid; + std::puts("the device has no multisampled storage view for the sample-order target; its retirement case is skipped"); + return; + } + SampleOrderTarget order; + VkImage oldImage = VK_NULL_HANDLE; + Image old{}; + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + order = MultisampleTargetOrderTarget(context, target); + old = ImageFor(MultisampleTargetView(context, target), &oldImage); + Require(order.image == oldImage, "the sample-order target does not name its target's image"); + } + Sync(); + block.Unmap(); + block.Map(); + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + const auto replacement = ImageFor(MultisampleTargetView(context, target)); + CheckPendingImages(); + Require(replacement.generation != old.generation, "a remapped reordered MSAA target reused its retired owner"); + Require(Alive(oldImage, old), "a held sample-order target lost its image when its MSAA target retired"); + context.recorder->Keep(order.keep); + order = {}; + Require(Alive(oldImage, old), "recorded sample-order work lost its image when its MSAA target retired"); + } + Sync(); + AwaitImages(1, "a retired sample-order target survives its last batch"); + Require(!Alive(oldImage, old), "a retired reordered MSAA parent survives its last batch"); + block.Unmap(); + Clear(); + context.sampleLocationSampleCounts = locations; + context.sampleLocationGridSampleCounts = grid; + AwaitImages(0, "reordered MSAA targets were not fully released"); + } + void StorageRemap() { GuestBlock block; const auto target = Target(block); @@ -487,7 +532,8 @@ int main() { run.RecordedRemap(); run.SampledRemap(); run.StorageRemap(); - std::puts("retired MSAA allocations stay bounded, recorded clears and storage promotions retain their images, and sampled aliases release their parents after the last batch"); + run.OrderRemap(); + std::puts("retired MSAA allocations stay bounded, recorded clears and storage promotions retain their images, sampled aliases release their parents after the last batch, and sample-order targets keep their retired images until their batch completes"); return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp index e20da7e63a..cda5b9280a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp @@ -534,7 +534,7 @@ std::array DepthSampleWords(const DepthTarget& target) { const auto format = target.format == VK_FORMAT_D16_UNORM ? 7u : 22u; const auto count = static_cast(std::countr_zero(target.samples)); const auto width = target.extent.width - 1u, height = target.extent.height - 1u; - return {static_cast(base), static_cast((base >> 32u) & 255u) | (format << 20u) | ((width & 3u) << 30u), (width >> 2u) | (height << 14u), (14u << 28u) | (24u << 20u) | (count << 16u) | 0x204u, 0u, count << 4u, (1u << 10u) | (1u << 19u) | (1u << 21u) | (static_cast(metadata & 255u) << 24u), static_cast(metadata >> 8u)}; + return {static_cast(base), static_cast((base >> 32u) & 255u) | (format << 20u) | ((width & 3u) << 30u), (width >> 2u) | (height << 14u), (14u << 28u) | (24u << 20u) | (count << 16u) | 0x204u, 0u, count << 4u, (target.iterate256 ? 1u << 10u : 0u) | (1u << 19u) | (1u << 21u) | (static_cast(metadata & 255u) << 24u), static_cast(metadata >> 8u)}; } bool DepthSupported(const Context& context, VkFormat format, std::uint32_t fragments) { @@ -649,7 +649,7 @@ void CheckClearDispatch() { } } -void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, bool one, bool clearing = false, std::optional depthOnlyOverride = std::nullopt, ClearFault fault = ClearFault::None) { +void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, bool one, bool clearing = false, std::optional depthOnlyOverride = std::nullopt, ClearFault fault = ClearFault::None, bool iterate256 = true) { const VkExtent2D extent{5u, 3u}; const auto bytes = static_cast(MultisampledDepthBytes_nid_no_patch(extent, format == VK_FORMAT_D16_UNORM ? 2u : 4u, fragments)); TargetMemory memory(bytes), metadata(65536u); @@ -661,7 +661,10 @@ void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, target.depthSwizzle = 24u; target.pipeBankXor = one ? 0x8c00u : 0u; target.htileDepthOnly = depthOnly; - target.iterate256 = target.htilePipeAligned = true; + target.iterate256 = iterate256; + target.htilePipeAligned = true; + TargetMemory otherMemory(static_cast(DepthSliceBytes(extent, 4u))); + DepthTarget otherTarget{otherMemory.Address(), 0u, extent, VK_FORMAT_D32_SFLOAT, 0.0f, 0u, 0u, false, 1u}; Recorder recorder(context); recorder.Activate(); struct Cleanup { @@ -733,10 +736,18 @@ void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, { const DepthRaster raster(context, target, view); for (const auto offset : {0.0f, 0.25f}) { + Require(DepthSurfaceView(context, target) == view, "rebinding the native depth attachment replaced its image"); Require(CachedSampledTexture(context, words) == texture && CachedSampledTexture(context, words, VK_IMAGE_VIEW_TYPE_2D_ARRAY) == arrayTexture, "native sampled view changed between raster writes"); std::memset(output.Bytes().data(), 0xa5, output.Bytes().size()); + { + CommandBatch batch(context); + raster.Run(batch.Handle(), offset); + batch.SubmitAndWait(); + } + if (offset == 0.0f) DepthSurfaceView(context, otherTarget); + Require(CachedSampledTexture(context, words) == texture, "native depth shadow view changed after a raster write"); + recorder.Sync(); CommandBatch batch(context); - raster.Run(batch.Handle(), offset); fetch.Run(batch.Handle(), count); RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); batch.SubmitAndWait(); @@ -814,7 +825,7 @@ void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, case 8u: changed.pipeBankXor ^= 0x100u; break; case 9u: changed.dccAddress += 256u; break; case 10u: changed.dccPipeAligned = false; break; - case 11u: changed.iterate256 = false; break; + case 11u: changed.iterate256 = !changed.iterate256; break; case 12u: changed.allocatedMipCount = 2u; break; case 13u: changed.mipCount = 2u; break; case 14u: changed.baseLevel = 1u; break; @@ -829,7 +840,7 @@ void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, TargetRefuses([&] { DepthSurfaceHolds(context, resource, texture.get()); }, "guest writes"); texture.reset(); arrayTexture.reset(); - std::printf("Native depth format=%u samples=%u clear=%u: imported, rasterized twice, sampled and refused conflicts\n", static_cast(format), fragments, one); + std::printf("Native depth format=%u samples=%u clear=%u iterate256=%u: imported, rasterized twice, sampled and refused conflicts\n", static_cast(format), fragments, one, iterate256); } void CheckHtileRouting(Context context, VkFormat format, std::uint32_t fragments) { @@ -895,7 +906,7 @@ void CheckEligibility(Context context) { case 2u: invalid.lastSlice = 1u; reason = "arrays"; break; case 3u: invalid.pipeBankXor = 0x10u; reason = "XOR"; break; case 4u: invalid.htileAddress = 0u; reason = "HTILE"; break; - case 5u: invalid.iterate256 = false; reason = "HTILE"; break; + case 5u: invalid.htileAddress += 16u; reason = "HTILE"; break; case 6u: invalid.htilePipeAligned = false; reason = "HTILE"; break; case 7u: invalid.htileVrsEncoding = 1u; reason = "VRS"; break; case 8u: invalid.clearDepth = 0.5f; reason = "clear value"; break; @@ -1163,8 +1174,10 @@ int main(int argc, char** argv) { std::uint32_t checked = 0u, unavailable = 0u; for (const auto format : {VK_FORMAT_D16_UNORM, VK_FORMAT_D32_SFLOAT}) { for (const auto samples : {2u, 4u, 8u}) { - if (!DepthSupported(context, format, samples)) { unavailable += 2u; continue; } - for (const bool one : {false, true}) { CheckDepthTarget(context, format, samples, one); ++checked; } + if (!DepthSupported(context, format, samples)) { unavailable += 4u; continue; } + for (const bool one : {false, true}) { + for (const bool iterate256 : {true, false}) { CheckDepthTarget(context, format, samples, one, false, std::nullopt, ClearFault::None, iterate256); ++checked; } + } CheckHtileRouting(context, format, samples); } } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/OversizedDispatch.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/OversizedDispatch.cpp index 06b14970f4..03d1ada000 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/OversizedDispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/OversizedDispatch.cpp @@ -24,6 +24,12 @@ namespace { +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + using AgcDriver::Graphics::Require; constexpr std::uint32_t Threads = 64; @@ -94,7 +100,7 @@ void CheckSplitCoverage(AgcDriver::VulkanDevice& device) { device.WaitIdle(); AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(guest.Data()), guest.Bytes); for (std::uint32_t i = 0; i < Stores; ++i) { - Require(guest.Data()[i] == Pattern[i % Period], "oversized dispatch: dword " + std::to_string(i) + " is 0x" + std::to_string(guest.Data()[i]) + ", expected 0x" + std::to_string(Pattern[i % Period])); + Require(guest.Data()[i] == Pattern[i % Period], "oversized dispatch: dword " + std::to_string(i) + " is " + Hex(guest.Data()[i]) + ", expected " + Hex(Pattern[i % Period])); } } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PartiallyResidentSnapshot.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PartiallyResidentSnapshot.cpp index 5d747bf85a..0afb33266f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PartiallyResidentSnapshot.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PartiallyResidentSnapshot.cpp @@ -45,6 +45,8 @@ constexpr std::uint32_t TileR64KBX = 0x1b; constexpr std::uint32_t Type2D = 9; constexpr std::size_t BlockBytes = 65536; constexpr std::size_t SurfaceBytes = std::size_t{Side} * Side * 4u; +constexpr std::uint32_t LargeSide = 512; +constexpr std::size_t LargeSurfaceBytes = std::size_t{LargeSide} * LargeSide * 4u; constexpr int Skipped = 77; class Device { @@ -149,16 +151,16 @@ private: class ReservedSurface { public: - ReservedSurface() { - block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(SurfaceBytes, BlockBytes)); + explicit ReservedSurface(std::size_t bytes) : size(bytes) { + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(size, BlockBytes)); Require(block != nullptr, "partially resident snapshot: cannot reserve the guest surface"); - GuestAllocations::Mutation().Add(block, SurfaceBytes, true, true, true); + GuestAllocations::Mutation().Add(block, size, true, true, true); } ~ReservedSurface() { GuestAllocations::Mutation().Remove(block); - Decommit(0, SurfaceBytes); - GuestArena::GuestArenaRelease_nid_postfix(block, SurfaceBytes); + Decommit(0, size); + GuestArena::GuestArenaRelease_nid_postfix(block, size); } ReservedSurface(const ReservedSurface&) = delete; @@ -189,6 +191,7 @@ public: std::uint64_t Address() const { return static_cast(reinterpret_cast(block)); } private: + std::size_t size = 0; std::uint8_t* block = nullptr; }; @@ -200,11 +203,11 @@ void CountReads(std::uint64_t address, std::size_t bytes) { if (address < surfaceEnd.load() && address + bytes > surfaceBegin.load()) surfaceReads.fetch_add(1); } -std::array Descriptor(std::uint64_t address) { +std::array Descriptor(std::uint64_t address, std::uint32_t side = Side) { return { static_cast(address >> 8u), - static_cast((address >> 40u) & 0xffu) | (Format32UInt << 20u) | (((Side - 1u) & 3u) << 30u), - ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), + static_cast((address >> 40u) & 0xffu) | (Format32UInt << 20u) | (((side - 1u) & 3u) << 30u), + ((side - 1u) >> 2u) | ((side - 1u) << 14u), 0xfacu | (TileR64KBX << 20u) | (Type2D << 28u), 0u, 0u, @@ -263,6 +266,48 @@ void Run(const Context& context, ReservedSurface& surface) { AgcDriver::GuestMemory::SetFlushHook(nullptr); } +void LargeTracked(const Context& context, ReservedSurface& surface, std::size_t committedBytes) { + const auto address = surface.Address(); + const auto words = Descriptor(address, LargeSide); + const auto layout = committedBytes == LargeSurfaceBytes ? std::string("committed") : std::string("partially resident"); + Require(DescribeSurface(DecodeTextureResource(words)).guestBytes == LargeSurfaceBytes, "the large test descriptor does not describe the reserved surface"); + surface.Commit(0, committedBytes); + std::memset(surface.Data(), 0x5a, committedBytes); + surfaceBegin = address; + surfaceEnd = address + LargeSurfaceBytes; + std::shared_ptr previous; + const auto sample = [&](const std::string& when, bool uploaded) { + AgcDriver::GuestMemory::BumpCollectEpoch(); + surfaceReads = 0; + auto texture = CachedSampledTexture(context, words); + Require(texture != nullptr && !texture->ViewsStorageImage(), "the large " + layout + " surface was not sampled through a snapshot " + when); + const auto reads = surfaceReads.load(); + if (uploaded && (reads == 0 || texture == previous)) throw std::runtime_error("sampling the large " + layout + " surface " + when + " kept the texture uploaded before it"); + if (!uploaded && (reads != 0 || texture != previous)) throw std::runtime_error("sampling the large " + layout + " surface " + when + " uploaded it again although the write tracker saw no write (" + std::to_string(reads) + " reads)"); + previous = texture; + }; + AgcDriver::GuestMemory::SetFlushHook(&CountReads); + try { + sample("at its first use", true); + sample("again", false); + surface.Data()[0x100] = 0x11; + sample("after a CPU write", true); + sample("after the upload of the written surface", false); + surface.Data()[committedBytes - 0x40] = 0x22; + sample("after a CPU write to its last committed block", true); + sample("after the upload of the written block", false); + AgcDriver::GuestMemory::MarkWritten(address + 0x200, 4); + sample("after a driver store of the bytes already there, which a snapshot it did not keep cannot tell from a change", true); + sample("after the upload that followed the driver store", false); + } catch (...) { + AgcDriver::GuestMemory::SetFlushHook(nullptr); + throw; + } + AgcDriver::GuestMemory::SetFlushHook(nullptr); + ClearCachedTextures(context.device); + surface.Decommit(0, committedBytes); +} + #ifdef _WIN32 void UntrackedHint(const Context& context, ReservedSurface& surface) { const auto address = surface.Address(); @@ -369,9 +414,13 @@ int main(int argc, char** argv) { auto context = device->GetContext(); TextureDetiler detiler(context); context.detiler = &detiler; - ReservedSurface surface; + const bool large = argc == 2 && std::string_view(argv[1]) == "--large-tracked"; + ReservedSurface surface(large ? LargeSurfaceBytes : SurfaceBytes); std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); - if (argc == 2 && std::string_view(argv[1]) == "--pending-stores") PendingStores(context, surface); + if (large) { + LargeTracked(context, surface, LargeSurfaceBytes); + LargeTracked(context, surface, BlockBytes); + } else if (argc == 2 && std::string_view(argv[1]) == "--pending-stores") PendingStores(context, surface); else if (argc == 2 && std::string_view(argv[1]) == "--untracked-hint") { #ifdef _WIN32 UntrackedHint(context, surface); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PassInputHazard.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PassInputHazard.cpp index 66e6aff3eb..cbe0f54f42 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PassInputHazard.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PassInputHazard.cpp @@ -223,7 +223,7 @@ int main() { std::memcpy(indices, triangle.data(), sizeof(triangle)); const auto variant = std::string(readDepth == nullptr ? "without depth" : writeDepth == nullptr ? "with depth on the reader" : "with a depth target each"); Draw(*device, reader, read, readDepth, indices); - Require(WriterFenced(inputs.Address(), sizeof(triangle)), "pass input hazard " + variant + ": a pass writing the indices an open pass reads in place took no barrier"); + if (device->DrawInputsInPlace()) Require(WriterFenced(inputs.Address(), sizeof(triangle)), "pass input hazard " + variant + ": a pass writing the indices an open pass reads in place took no barrier"); Draw(*device, reader, read, readDepth, indices); Draw(*device, writer, written, writeDepth, nullptr); Require(Covered(*device, read) == all, "pass input hazard " + variant + ": the draw reading its indices in place did not cover its target, a later pass overwrote them first"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedFill.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedFill.cpp index ff6892f765..1c13bc169d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedFill.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedFill.cpp @@ -76,11 +76,14 @@ void AppendLabel(std::vector& words, std::uint32_t* target, std:: struct Blocker { std::atomic release{false}; - void Enqueue() { + std::uint32_t* target = nullptr; + ~Blocker() { release = true; } + void Enqueue(std::vector writes = {}) { DrawPipeline::Queue0().Enqueue([this] { while (!release.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + if (target != nullptr) std::atomic_ref(*target).store(1); std::lock_guard gpuLock(AgcDriver::GuestMemory::GpuMutex()); - }, {}); + }, std::move(writes)); } }; @@ -157,6 +160,66 @@ void FillIntoFullPipeline() { for (std::size_t i = 0; i < Output.size(); ++i) Require(Output[i] == (i < 256 ? 0x5a5a5a5au : 0xcafebabeu), "a fill behind a full draw pipeline did not land at " + std::to_string(i)); } +void SatisfiedWaitKeepsUnrelatedQueuedWork() { + alignas(8) static std::uint32_t gate = 1; + alignas(8) static std::uint32_t marker = 0; + marker = 0; + + Blocker blocker; + blocker.Enqueue(); + + const auto address = reinterpret_cast(&gate); + std::vector words{ + 0xc0053c00u, 0x13u, static_cast(address), static_cast(address >> 32u), + 1u, 0xffffffffu, 0x19u}; + AppendLabel(words, &marker, 1); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3); + while (DrawPipeline::Queue0().Outstanding() < 2 && std::chrono::steady_clock::now() < deadline) + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + const bool keptQueued = DrawPipeline::Queue0().Outstanding() >= 2; + blocker.release = true; + Finish(); + Require(keptQueued, "a satisfied WAIT_REG_MEM drained unrelated queued work"); + Require(marker == 1, "a label after a satisfied WAIT_REG_MEM did not execute"); +} + +void SatisfiedWaitDrainsOverlappingQueuedWork() { + alignas(8) static std::uint32_t gate = 1; + alignas(8) static std::uint32_t marker = 0; + std::atomic_ref(gate).store(1); + marker = 0; + + Blocker blocker; + blocker.target = &gate; + const auto address = reinterpret_cast(&gate); + blocker.Enqueue({{address, address + sizeof(gate)}}); + + std::vector words{ + 0xc0053c00u, 0x13u, static_cast(address), static_cast(address >> 32u), + 1u, 0xffffffffu, 0x19u}; + AppendLabel(words, &marker, 1); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + const auto drainsBefore = DrawPipeline::DrainRequestsByOpcode(0x3c); + auto submitted = std::async(std::launch::async, [&] { AgcDriver::Submit(&packet, 0); }); + + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3); + while (DrawPipeline::DrainRequestsByOpcode(0x3c) == drainsBefore && + DrawPipeline::Queue0().Outstanding() < 2 && std::chrono::steady_clock::now() < deadline) + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + const bool drained = DrawPipeline::DrainRequestsByOpcode(0x3c) > drainsBefore; + const bool labelQueuedEarly = DrawPipeline::Queue0().Outstanding() >= 2; + blocker.release = true; + Require(submitted.wait_for(std::chrono::seconds(20)) == std::future_status::ready, + "WAIT_REG_MEM submission did not finish after the overlapping write"); + submitted.get(); + Finish(); + Require(drained && !labelQueuedEarly, "a satisfied WAIT_REG_MEM skipped an overlapping queued write"); + Require(marker == 1, "a label after an overlapping WAIT_REG_MEM did not execute"); +} + } int main() { @@ -172,6 +235,8 @@ int main() { } InputsAfterQueuedLabels(); FillIntoFullPipeline(); + SatisfiedWaitKeepsUnrelatedQueuedWork(); + SatisfiedWaitDrainsOverlappingQueuedWork(); AgcDriverShutdown_nid_postfix(); std::puts("pipelined fill tests passed"); return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedIndirectDraw.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedIndirectDraw.cpp new file mode 100644 index 0000000000..437a8ecaa2 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedIndirectDraw.cpp @@ -0,0 +1,259 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using AgcDriver::DriverDetail::DrawPipeline; + +constexpr std::byte Kept{0x40}; +constexpr std::uint32_t BaseVertexLocation = 0x8c; +constexpr std::uint32_t Clipped = 0x40000000u; + +alignas(256) constexpr std::array VertexCode{ + 0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u, + 0x7e060208u, 0x7e0802f2u, 0x7e0a02ffu, 0x44800000u, 0xf80000d1u, 0x00000005u, 0xf80008cfu, 0x04030201u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u, +}; +alignas(256) constexpr std::array PixelCode{0x7e000200u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u}; +alignas(256) std::array DirectMemory{}; +alignas(256) std::array FirstMemory{}; +alignas(256) std::array SecondMemory{}; +alignas(256) std::array WrittenMemory{}; +alignas(256) std::array Records{3, 1, 0, 0}; +alignas(256) std::array LateRecords{0, 1, 0, 0}; + +struct Header { + Shader shader{}; + ShaderUserData userData{}; +}; + +alignas(8) Header VertexHeader{}; +alignas(8) Header PixelHeader{}; + +void Registered(Header& header, const std::uint32_t* code, std::size_t bytes, std::uint8_t type) { + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18u; + header.shader.header_size = sizeof(Header); + header.shader.shader_size = static_cast(bytes); + header.shader.code = code; + header.shader.type = type; + header.shader.user_data = &header.userData; + AgcDriverRegisterShader_nid_postfix(&header.shader); +} + +void Append(std::vector& commands, std::uint32_t opcode, std::initializer_list payload) { + commands.push_back(0xc0000000u | (static_cast(payload.size() - 1) << 16u) | (opcode << 8u)); + commands.insert(commands.end(), payload); +} + +void AppendState(std::vector& commands) { + const auto vertex = reinterpret_cast(VertexCode.data()); + const auto pixel = reinterpret_cast(PixelCode.data()); + const std::vector> context{ + {0x2d5, 0x2000}, + {0x1b6, 0}, {0x207, 0x01200000}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, + {0x31b, 0}, {0x31d, 0}, {0x3b0, (63u << 14u) | 3u}, + {0x3b8, 0x9000000}, {0x1e0, 0}, + {0xc, 0}, {0xd, 0x40040}, + {0x81, 0x80000000}, {0x82, 0x40040}, + {0x90, 0x80000000}, {0x91, 0x40040}, + {0x94, 0x80000000}, {0x95, 0x40040}, + {0x10f, std::bit_cast(32.0f)}, {0x110, std::bit_cast(32.0f)}, + {0x111, std::bit_cast(-2.0f)}, {0x112, std::bit_cast(2.0f)}, + {0x113, std::bit_cast(1.0f)}, {0x114, 0}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)}, + {0x2fe, 0}, {0x302, 0}, {0x306, 0}, {0x30a, 0}, + }; + const std::vector> shader{ + {0xc8, static_cast(vertex >> 8u)}, {0xc9, static_cast(vertex >> 40u)}, {0x8b, 1u << 1u}, + {0x008, static_cast(pixel >> 8u)}, {0x009, static_cast(pixel >> 40u)}, {0x00b, 1u << 1u}, {0x00c, std::bit_cast(1.0f)}, + }; + Append(commands, 0x79, {0x242, 4}); + Append(commands, 0x79, {0x24a, 0}); + for (const auto& [offset, value] : context) Append(commands, 0x69, {offset, value}); + for (const auto& [offset, value] : shader) Append(commands, 0x76, {offset, value}); +} + +void AppendTarget(std::vector& commands, std::array& memory, std::uint32_t userWord) { + const auto color = reinterpret_cast(memory.data()); + Append(commands, 0x69, {0x318, static_cast(color >> 8u)}); + Append(commands, 0x69, {0x390, static_cast(color >> 40u)}); + Append(commands, 0x76, {BaseVertexLocation, userWord}); +} + +void AppendRecords(std::vector& commands, const void* records) { + const auto address = reinterpret_cast(records); + Append(commands, 0x11, {1, static_cast(address), static_cast(address >> 32u)}); +} + +void AppendIndirect(std::vector& commands) { + Append(commands, 0x24, {0, BaseVertexLocation, 0, 2}); +} + +void AppendLabel(std::vector& commands, std::uint32_t* target, std::uint32_t value) { + const auto address = reinterpret_cast(target); + commands.insert(commands.end(), {0xc0064900u, 0x514u, (1u << 29u) | (2u << 24u), static_cast(address), static_cast(address >> 32u), value, 0, 0}); +} + +std::size_t Covered(const std::array& memory) { + return static_cast(std::count_if(memory.begin(), memory.end(), [](std::byte value) { return value != Kept; })); +} + +void SameAsDirect(const std::array& memory, const std::string& what) { + for (std::size_t index = 0; index < DirectMemory.size(); ++index) { + Require(memory[index] == DirectMemory[index], what + ": byte " + std::to_string(index) + " is " + std::to_string(static_cast(memory[index])) + ", the direct draw wrote " + std::to_string(static_cast(DirectMemory[index]))); + } +} + +void Flush(const std::array& memory) { + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(memory.data()), memory.size()); +} + +struct Blocker { + std::atomic release{false}; + void Enqueue() { + DrawPipeline::Queue0().Enqueue([this] { + while (!release.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + std::lock_guard gpuLock(AgcDriver::GuestMemory::GpuMutex()); + }, {}); + } +}; + +void SubmitBehind(Blocker& blocker, std::vector& commands) { + std::unique_lock gpuLock(AgcDriver::GuestMemory::GpuMutex()); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + blocker.Enqueue(); +} + +void Finish() { + auto idle = std::async(std::launch::async, [] { AgcDriverWaitIdle_nid_postfix(); }); + if (idle.wait_for(std::chrono::seconds(20)) != std::future_status::ready) { + std::fputs("pipelined indirect draw: the queue worker and the draw pipeline deadlocked\n", stderr); + std::fflush(stderr); + std::_Exit(1); + } + idle.get(); +} + +void DrawDirectReference() { + DirectMemory.fill(Kept); + std::vector commands; + AppendState(commands); + AppendTarget(commands, DirectMemory, 0); + Append(commands, 0x2d, {3, 2}); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "the direct draw submission failed"); + AgcDriverWaitIdle_nid_postfix(); + Flush(DirectMemory); + const auto direct = Covered(DirectMemory); + Require(direct != 0 && direct != DirectMemory.size(), "the direct draw covered " + std::to_string(direct) + " of " + std::to_string(DirectMemory.size()) + " bytes, expected an edge through the target"); +} + +void IndirectDrawsQueueBehindTheCommitter() { + FirstMemory.fill(Kept); + SecondMemory.fill(Kept); + std::vector commands; + AppendState(commands); + AppendRecords(commands, Records.data()); + AppendTarget(commands, FirstMemory, Clipped); + AppendIndirect(commands); + AppendTarget(commands, SecondMemory, Clipped); + AppendIndirect(commands); + Blocker blocker; + SubmitBehind(blocker, commands); + const auto start = std::chrono::steady_clock::now(); + while (DrawPipeline::Queue0().Outstanding() < 3) { + if (std::chrono::steady_clock::now() - start > std::chrono::seconds(10)) { + const auto outstanding = DrawPipeline::Queue0().Outstanding(); + blocker.release = true; + Finish(); + Require(false, "the indirect draws drained the pipeline instead of queuing behind it (" + std::to_string(outstanding) + " items outstanding, expected the blocker and two draws)"); + } + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + blocker.release = true; + Finish(); + Flush(FirstMemory); + Flush(SecondMemory); + SameAsDirect(FirstMemory, "the first queued indirect draw"); + SameAsDirect(SecondMemory, "the second queued indirect draw"); +} + +void IndirectDrawWaitsForItsQueuedRecordWrite() { + WrittenMemory.fill(Kept); + LateRecords = {0, 1, 0, 0}; + std::vector commands; + AppendState(commands); + AppendLabel(commands, &LateRecords[0], 3); + AppendRecords(commands, LateRecords.data()); + AppendTarget(commands, WrittenMemory, Clipped); + AppendIndirect(commands); + Blocker blocker; + SubmitBehind(blocker, commands); + const auto start = std::chrono::steady_clock::now(); + while (DrawPipeline::Queue0().Outstanding() < 2 && std::chrono::steady_clock::now() - start < std::chrono::seconds(10)) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + blocker.release = true; + Finish(); + Flush(WrittenMemory); + Require(LateRecords[0] == 3, "the queued label did not store the record's vertex count"); + SameAsDirect(WrittenMemory, "the indirect draw whose record a queued label wrote"); +} + +} + +int main() { +#ifdef _WIN32 + _putenv_s("APS5_PIPELINED_DRAWS", "1"); +#else + setenv("APS5_PIPELINED_DRAWS", "1", 1); +#endif + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + Registered(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2); + Registered(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1); + DrawDirectReference(); + IndirectDrawsQueueBehindTheCommitter(); + IndirectDrawWaitsForItsQueuedRecordWrite(); + AgcDriverShutdown_nid_postfix(); + std::puts("pipelined indirect draw tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PixelBarycentricEmulation.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelBarycentricEmulation.cpp index 9978674554..f9813bbe14 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PixelBarycentricEmulation.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelBarycentricEmulation.cpp @@ -35,6 +35,10 @@ alignas(256) constexpr std::array VertexValuePixelCode{ 0xc80a0000, 0xc80e0001, 0xc8120002, 0xf800180f, 0x04040302, 0xbf810000, }; +alignas(256) constexpr std::array FlatBarycentricPixelCode{ + 0xc8120002, 0xf800180f, 0x04040100, 0xbf810000, +}; + alignas(256) constexpr std::array InterpolatedPixelCode{ 0xc8080000, 0xc8090001, 0xf800180f, 0x02020202, 0xbf810000, }; @@ -63,7 +67,7 @@ enum class Emulation { Tessellation, }; -ShaderRecompiler::RecompileResult Draw(AgcDriver::VulkanDevice& device, Emulation emulation, std::span pixelCode, std::uint32_t inputs, VkCullModeFlags cullMode = VK_CULL_MODE_NONE) { +ShaderRecompiler::RecompileResult Draw(AgcDriver::VulkanDevice& device, Emulation emulation, std::span pixelCode, std::uint32_t inputs, VkCullModeFlags cullMode = VK_CULL_MODE_NONE, bool flat = false, VkProvokingVertexModeEXT provoking = VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT) { Pixels.fill(Unwritten); auto target = device.Target(); target.fragmentShaderBarycentricEnabled = false; @@ -85,6 +89,7 @@ ShaderRecompiler::RecompileResult Draw(AgcDriver::VulkanDevice& device, Emulatio ShaderRecompiler::ShaderPixelStageInfo pixel{}; pixel.interpolatorCount = 1; + if (flat) pixel.interpolatorSettings[0] = 0x400u; pixel.inputAddr = inputs; pixel.hasPerspectiveCenterVgpr = (inputs & PixelInputBit(PixelInput::PerspectiveCenter)) != 0u; pixel.noPerspective = (inputs & PixelInputBit(PixelInput::LinearCenter)) != 0u; @@ -108,7 +113,7 @@ ShaderRecompiler::RecompileResult Draw(AgcDriver::VulkanDevice& device, Emulatio geometry = ShaderRecompiler::BuildBarycentricGeometryShader(vertexResult, pixelResult, target, device.GeometryLimits()); shaders.push_back({ShaderStage::Geometry, &geometry, 0}); } else if (pixelResult.barycentricEmulation.active) { - patches = ShaderRecompiler::BuildBarycentricTessellationShaders(vertexResult, pixelResult, target, device.MaxFragmentInputComponents()); + patches = ShaderRecompiler::BuildBarycentricTessellationShaders(vertexResult, pixelResult, target, device.MaxFragmentInputComponents(), provoking == VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT ? 2u : 0u); shaders.push_back({ShaderStage::TessellationControl, &patches.control, 0}); shaders.push_back({ShaderStage::TessellationEvaluation, &patches.evaluation, 0}); } @@ -125,6 +130,7 @@ ShaderRecompiler::RecompileResult Draw(AgcDriver::VulkanDevice& device, Emulatio state.negativeOneToOne = false; state.scissor = {{0, 0}, {Width, Height}}; state.cullMode = cullMode; + state.provokingVertexMode = provoking; state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; state.blend.colorWriteMask = 15; state.blends = {state.blend}; @@ -242,6 +248,26 @@ void CheckInterpolated() { Require(checked > 200u, "v_interp_p1/p2: too few interior pixels were checked"); } +void CheckFlat(const std::string& what, std::size_t provoking) { + const auto positions = ClipPositions(); + std::uint32_t checked = 0; + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto expected = BarycentricsUnsnapped(x, y); + if (!expected.inside) continue; + const auto snapped = BarycentricsSnapped(x, y); + for (std::uint32_t channel = 0; channel < 2; ++channel) { + Require(MatchesEither(Pixel(x, y, channel), expected.perspective[1u + channel], snapped.perspective[1u + channel]), what + ": pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") weight " + std::to_string(channel) + " is " + std::to_string(Pixel(x, y, channel))); + } + for (std::uint32_t channel = 2; channel < 4; ++channel) { + Require(Pixel(x, y, channel) == positions[provoking][0], what + ": pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") flat channel " + std::to_string(channel) + " is " + std::to_string(Pixel(x, y, channel)) + ", expected vertex " + std::to_string(provoking) + "'s " + std::to_string(positions[provoking][0])); + } + ++checked; + } + } + Require(checked > 200u, what + ": too few interior pixels were checked"); +} + bool Drawn() { return std::any_of(Pixels.begin(), Pixels.end(), [](float value) { return value != Unwritten; }); } @@ -273,7 +299,14 @@ void RunEmulation(AgcDriver::VulkanDevice& device, Emulation emulation) { Require(!result.barycentricEmulation.active, "plain v_interp_p1/p2 needed a generated stage"); CheckInterpolated(); CheckWinding(device, emulation, name); - std::printf("barycentric emulation through the %s passed: %u perspective and %u linear I/J pixels, raw vertex values, host interpolation and face culling\n", name.c_str(), smooth, noPerspective); + result = Draw(device, emulation, FlatBarycentricPixelCode, perspective, VK_CULL_MODE_NONE, true); + Require(result.barycentricEmulation.active && std::any_of(result.fragmentParameters.begin(), result.fragmentParameters.end(), [](const auto& parameter) { return parameter.flat && !parameter.perVertex; }), "a flat P0 read beside raw I/J did not pass a flat parameter through the emulation"); + CheckFlat(name + " flat input, first provoking vertex", 0u); + if (emulation == Emulation::Tessellation) { + Draw(device, emulation, FlatBarycentricPixelCode, perspective, VK_CULL_MODE_NONE, true, VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT); + CheckFlat(name + " flat input, last provoking vertex", 2u); + } + std::printf("barycentric emulation through the %s passed: %u perspective and %u linear I/J pixels, raw vertex values, host interpolation, face culling and flat inputs\n", name.c_str(), smooth, noPerspective); } } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PixelSampleIteration.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelSampleIteration.cpp index e5b8af3bbb..072b3ca41b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PixelSampleIteration.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelSampleIteration.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -99,6 +100,7 @@ struct Pass { std::uint32_t barycentricControl = 0; std::array quadSampleMask{0xffffu, 0xffffu, 0xffffu, 0xffffu}; bool customLocations = false; + AgcDriver::Graphics::SampleOrder order{}; }; std::uint8_t CustomLocation(std::uint32_t pixel, std::uint32_t sample) { @@ -132,9 +134,10 @@ ShaderRecompiler::ShaderPixelStageInfo PassPixel(const Pass& pass, const AgcDriv const AgcDriver::Registers context{{0x1b3, pass.inputs}, {0x1b4, pass.inputs}, {0x1b6, 0u}, {0x1b8, pass.barycentricControl}, {0x1c5, 0u}, {0x203, 0u}}; auto pixel = AgcDriver::Graphics::DecodePixelStageInfo(context, AgcDriver::Graphics::ExportMappings(state)); pixel.quadPixelMask = AgcDriver::Graphics::QuadPixelMask(state); - pixel.quadSampleMask = AgcDriver::Graphics::QuadSampleMasks(state); + pixel.quadSampleMask = AgcDriver::Graphics::QuadSampleMasks(state, pass.order); pixel.iteratesSamples = state.perSampleShading; pixel.sampleLocations = AgcDriver::Graphics::PixelSampleLocations(state, pixel); + pixel.sampleOrder = pass.order; return pixel; } @@ -271,6 +274,9 @@ struct Module { std::vector> offsets; std::vector>> tables; bool variableOffset = false; + std::map decorations; + std::map constants; + std::map>> instructions; }; Module PixelModule(const Pass& pass, bool interpolationFunctions) { @@ -292,15 +298,38 @@ Module PixelModule(const Pass& pass, bool interpolationFunctions) { request.useCache = false; const auto result = ShaderRecompiler::Recompile(request); const auto& words = result.spirv.Words(); - std::map decorations; std::map floats; std::map> vectors; Module module; for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { const auto op = static_cast(words[at] & 0xffffu); + const auto count = static_cast(words[at] >> 16u); if (op == spv::OpCapability) module.capabilities.push_back(static_cast(words[at + 1])); - if (op == spv::OpDecorate && words[at + 2] == spv::DecorationBuiltIn) decorations[words[at + 1]] = static_cast(words[at + 3]); - if (op == spv::OpConstant && (words[at] >> 16u) == 4u) floats[words[at + 2]] = std::bit_cast(words[at + 3]); + if (op == spv::OpDecorate && words[at + 2] == spv::DecorationBuiltIn) module.decorations[words[at + 1]] = static_cast(words[at + 3]); + if (op == spv::OpConstant && count == 4u) { + floats[words[at + 2]] = std::bit_cast(words[at + 3]); + module.constants[words[at + 2]] = words[at + 3]; + } + switch (op) { + case spv::OpConstant: + case spv::OpLoad: + case spv::OpAccessChain: + case spv::OpBitcast: + case spv::OpConvertFToU: + case spv::OpBitwiseAnd: + case spv::OpBitwiseOr: + case spv::OpShiftLeftLogical: + case spv::OpShiftRightLogical: + case spv::OpIAdd: + case spv::OpIEqual: + case spv::OpSelect: + case spv::OpCompositeExtract: + case spv::OpExtInst: + if (count >= 3u) module.instructions[words[at + 2]] = {op, std::vector(words.begin() + static_cast(at) + 3, words.begin() + static_cast(at) + static_cast(count))}; + break; + default: + break; + } if (op == spv::OpConstantComposite && (words[at] >> 16u) == 5u && floats.contains(words[at + 3]) && floats.contains(words[at + 4])) vectors[words[at + 2]] = {floats[words[at + 3]], floats[words[at + 4]]}; if (op == spv::OpConstantComposite && (words[at] >> 16u) > 5u && vectors.contains(words[at + 3])) { std::vector> table; @@ -314,8 +343,8 @@ Module PixelModule(const Pass& pass, bool interpolationFunctions) { } if (op != spv::OpLoad && op != spv::OpAccessChain && op != spv::OpExtInst) continue; const auto pointer = op == spv::OpExtInst ? words[at + 5] : words[at + 3]; - const auto found = decorations.find(pointer); - if (found != decorations.end() && std::ranges::find(module.builtins, found->second) == module.builtins.end()) module.builtins.push_back(found->second); + const auto found = module.decorations.find(pointer); + if (found != module.decorations.end() && std::ranges::find(module.builtins, found->second) == module.builtins.end()) module.builtins.push_back(found->second); } return module; } @@ -384,6 +413,136 @@ void ExpectModules() { Require(module.offsets.empty() && !Has(module, spv::BuiltInSampleId), "1x: a single-sample pixel shader changed"); } +const std::pair>* Instruction(const Module& module, std::uint32_t id) { + const auto found = module.instructions.find(id); + return found == module.instructions.end() ? nullptr : &found->second; +} + +bool ConstantIs(const Module& module, std::uint32_t id, std::uint32_t value) { + const auto found = module.constants.find(id); + return found != module.constants.end() && found->second == value; +} + +std::set BuiltinTrace(const Module& module, std::uint32_t id) { + std::set found; + std::set seen; + std::vector pending{id}; + while (!pending.empty()) { + const auto cursor = pending.back(); + pending.pop_back(); + if (!seen.insert(cursor).second) continue; + const auto* instruction = Instruction(module, cursor); + if (instruction == nullptr) continue; + if ((instruction->first == spv::OpLoad || instruction->first == spv::OpAccessChain) && !instruction->second.empty()) { + const auto decoration = module.decorations.find(instruction->second.front()); + if (decoration != module.decorations.end()) found.insert(decoration->second); + } + for (const auto argument : instruction->second) pending.push_back(argument); + } + return found; +} + +std::uint32_t GuestIndexOrderWord(const Module& module, std::uint32_t id) { + const auto* masked = Instruction(module, id); + if (masked == nullptr || masked->first != spv::OpBitwiseAnd || masked->second.size() != 2u || !ConstantIs(module, masked->second[1], 7u)) return 0u; + const auto* shifted = Instruction(module, masked->second[0]); + if (shifted == nullptr || shifted->first != spv::OpShiftRightLogical || shifted->second.size() != 2u) return 0u; + const auto* shift = Instruction(module, shifted->second[1]); + if (shift == nullptr || shift->first != spv::OpShiftLeftLogical || shift->second.size() != 2u || !ConstantIs(module, shift->second[1], 2u)) return 0u; + const auto* low = Instruction(module, shift->second[0]); + if (low == nullptr || low->first != spv::OpBitwiseAnd || low->second.size() != 2u || !ConstantIs(module, low->second[1], 7u)) return 0u; + const auto* bits = Instruction(module, low->second[0]); + if (bits == nullptr || bits->first != spv::OpBitcast || bits->second.empty()) return 0u; + const auto* loaded = Instruction(module, bits->second[0]); + if (loaded == nullptr || loaded->first != spv::OpLoad || loaded->second.empty()) return 0u; + const auto decoration = module.decorations.find(loaded->second[0]); + if (decoration == module.decorations.end() || decoration->second != spv::BuiltInSampleId) return 0u; + return shifted->second[0]; +} + +bool OrderWordValues(const Module& module, std::uint32_t id, std::set& values) { + const auto constant = module.constants.find(id); + if (constant != module.constants.end()) { + values.insert(constant->second); + return true; + } + const auto* instruction = Instruction(module, id); + if (instruction == nullptr || instruction->first != spv::OpSelect || instruction->second.size() != 3u) return false; + return OrderWordValues(module, instruction->second[1], values) && OrderWordValues(module, instruction->second[2], values); +} + +void CheckOrderIndex(const Module& module, const AgcDriver::Graphics::SampleOrder& order, const std::string& what) { + std::vector indexes; + for (const auto& [id, instruction] : module.instructions) { + if (instruction.first != spv::OpExtInst || instruction.second.size() < 4u || instruction.second[1] != GLSLstd450InterpolateAtOffset) continue; + const auto* loaded = Instruction(module, instruction.second[3]); + if (loaded == nullptr || loaded->first != spv::OpLoad || loaded->second.empty()) continue; + const auto* chain = Instruction(module, loaded->second[0]); + if (chain == nullptr || chain->first != spv::OpAccessChain || chain->second.size() < 2u) continue; + indexes.push_back(chain->second[1]); + } + Require(!indexes.empty(), what + ": the off-center sample read does not go through the per-sample location table"); + for (const auto index : indexes) { + const auto* joined = Instruction(module, index); + Require(joined != nullptr && joined->first == spv::OpBitwiseOr && joined->second.size() == 2u, what + ": the location table index is not guest | (quad << 3)"); + std::uint32_t quad = 0u; + std::uint32_t guest = 0u; + for (const auto operand : joined->second) { + const auto* instruction = Instruction(module, operand); + if (instruction != nullptr && instruction->first == spv::OpShiftLeftLogical && instruction->second.size() == 2u && ConstantIs(module, instruction->second[1], 3u)) quad = operand; + else guest = operand; + } + Require(quad != 0u && guest != 0u, what + ": the location table index is not guest | (quad << 3)"); + Require(BuiltinTrace(module, quad).contains(spv::BuiltInFragCoord), what + ": the table index's quad term does not come from the position input"); + const auto word = GuestIndexOrderWord(module, guest); + Require(word != 0u, what + ": the table index's low bits are not the order word's nibble at the shader's SampleId"); + std::set values; + Require(OrderWordValues(module, word, values), what + ": the order word in the table index is not selected per 2x2 pixel"); + for (const auto value : values) { + Require(std::ranges::any_of(order, [&](std::uint32_t word32) { return value == (word32 == 0u ? AgcDriver::Graphics::KeptSampleOrder : word32); }), what + ": the table index selects an order word the pass did not carry"); + } + } +} + +void ExpectOrderModules() { + const AgcDriver::Graphics::SampleOrder order{0x76543201u, AgcDriver::Graphics::KeptSampleOrder, 0x76543201u, AgcDriver::Graphics::KeptSampleOrder}; + for (const std::uint32_t samples : {2u, 4u}) { + const auto name = std::to_string(samples) + "x"; + Pass pass; + pass.samples = samples; + pass.inputs = Village; + pass.iterated = true; + pass.customLocations = true; + pass.order = order; + const auto module = PixelModule(pass, true); + Require(Has(module, spv::BuiltInSampleId) && Has(module, spv::BuiltInFragCoord), name + ": the reordered offset read lost the per-sample index or the position input"); + const auto customTable = [&](const std::vector>& table) { + if (table.size() != 32u) return false; + for (std::uint32_t index = 0; index < table.size(); ++index) { + const auto location = index % 8u < samples ? CustomLocation(index / 8u, index % 8u) : std::uint8_t{0u}; + if (table[index][0] != ShaderRecompiler::SampleLocationOffset(location, 0u) || table[index][1] != ShaderRecompiler::SampleLocationOffset(location, 1u)) return false; + } + return true; + }; + Require(std::ranges::any_of(module.tables, customTable), name + ": no 32-entry table of every pixel's custom sample locations"); + CheckOrderIndex(module, order, name + ": an iterated PERSP_SAMPLE on a reordered pattern varying across the 2x2 grid"); + } + for (const std::uint32_t samples : {2u, 4u}) { + const auto name = std::to_string(samples) + "x"; + for (const bool iterated : {false, true}) { + Pass pass; + pass.samples = samples; + pass.inputs = Ancillary; + pass.iterated = iterated; + pass.order = order; + Require(Has(PixelModule(pass, true), spv::BuiltInFragCoord), name + ": a pixel shader that only reorders its samples was not given the position input"); + pass.order = {}; + Require(!Has(PixelModule(pass, true), spv::BuiltInFragCoord), name + ": a pixel shader with nothing needing a position got one anyway"); + } + } + std::puts("recompiled modules index each pixel's location table by the guest sample index and the 2x2 quad term, and request the position input for the sample order alone"); +} + void ExpectSampleLocations() { Pass pass; for (const std::uint32_t samples : {1u, 2u, 4u, 8u}) { @@ -422,6 +581,7 @@ int main() { try { ExpectSampleLocations(); ExpectModules(); + ExpectOrderModules(); const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; Require(TargetHasCapability(device->Target(), spv::CapabilityInterpolationFunction), "the test device lacks sampleRateShading"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PolygonModeDraw.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PolygonModeDraw.cpp new file mode 100644 index 0000000000..db8de1f769 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PolygonModeDraw.cpp @@ -0,0 +1,184 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 16; +constexpr std::uint32_t WaveSize = 64; +constexpr std::byte Kept{0x40}; +constexpr std::byte Written{0xff}; +alignas(256) std::array Pixels{}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array PixelCode{ + 0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000, +}; + +using Pixel = std::pair; + +constexpr std::array, 3> Corners{{{4.5f, 2.5f}, {40.5f, 2.5f}, {4.5f, 12.5f}}}; + +const std::set CornerPixels{{4, 2}, {40, 2}, {4, 12}}; +const std::set Interior{{10, 4}, {20, 4}, {15, 5}, {8, 8}}; +const std::set Outside{{50, 10}, {60, 2}, {30, 12}, {2, 2}, {4, 14}}; + +alignas(256) std::array, Corners.size()> Vertices{}; + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +void Draw(AgcDriver::VulkanDevice& device, VkPolygonMode mode) { + Pixels.fill(Kept); + for (std::size_t index = 0; index < Corners.size(); ++index) { + Vertices[index] = {Corners[index][0] * 2.0f / Width - 1.0f, 1.0f - Corners[index][1] * 2.0f / Height, 0.5f, 1.0f}; + } + const auto target = device.Target(); + + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(Vertices.data(), 16u, static_cast(Vertices.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {WaveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY); + pixel.posX = true; + pixel.posY = true; + pixel.targetOutputMode[0] = 9; + pixel.targetExportMapping.fill(0xe4u); + const std::vector pixelUserData(8, 0u); + const std::array pixelMemory{{{reinterpret_cast(PixelCode.data()), std::as_bytes(std::span(PixelCode))}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(PixelCode.data()), PixelCode, 0, {}}, + {WaveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, WaveSize, WaveSize, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.polygonMode = mode; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Vertices.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +bool IsWritten(std::uint32_t x, std::uint32_t y) { + bool written = true; + bool kept = true; + for (std::uint32_t channel = 0; channel < 4; ++channel) { + const auto value = Pixels[(y * Width + x) * 4u + channel]; + written = written && value == Written; + kept = kept && value == Kept; + } + Require(written || kept, "pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") is neither written nor kept"); + return written; +} + +void Expect(const std::set& pixels, bool written, std::string_view what) { + for (const auto& [x, y] : pixels) { + Require(IsWritten(x, y) == written, std::string(what) + ": pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") is " + (written ? "kept" : "written") + ", expected " + (written ? "written" : "kept")); + } +} + +void ExpectExactly(const std::set& pixels, std::string_view what) { + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const bool expected = pixels.contains({x, y}); + Require(IsWritten(x, y) == expected, std::string(what) + ": pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") is " + (expected ? "kept" : "written") + ", expected " + (expected ? "written" : "kept")); + } + } +} + +std::set StraightEdges() { + std::set pixels; + for (std::uint32_t x = 5; x <= 39; ++x) pixels.insert({x, 2}); + for (std::uint32_t y = 3; y <= 11; ++y) pixels.insert({4, y}); + return pixels; +} + +bool Unsupported(AgcDriver::VulkanDevice& device, VkPolygonMode mode) { + try { + Draw(device, mode); + return false; + } catch (const std::exception& error) { + const std::string_view message = error.what(); + if (message.find("needs the fillModeNonSolid feature") != std::string_view::npos || message.find("needs VK_KHR_maintenance5") != std::string_view::npos) { + std::printf("skipped, %s\n", error.what()); + return true; + } + throw; + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Draw(*device, VK_POLYGON_MODE_FILL); + Expect(Interior, true, "filled triangle"); + Expect(Outside, false, "filled triangle"); + if (Unsupported(*device, VK_POLYGON_MODE_POINT)) return VulkanTestSkipped; + ExpectExactly(CornerPixels, "point polygon mode"); + if (Unsupported(*device, VK_POLYGON_MODE_LINE)) return VulkanTestSkipped; + Expect(StraightEdges(), true, "line polygon mode"); + Expect(Interior, false, "line polygon mode"); + Expect(Outside, false, "line polygon mode"); + Draw(*device, VK_POLYGON_MODE_FILL); + Expect(Interior, true, "filled triangle after the point and line modes"); + std::puts("point and line polygon modes drew the triangle's corners and edges"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PooledBufferRange.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PooledBufferRange.cpp index 13e19d7dcb..785886aa84 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PooledBufferRange.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PooledBufferRange.cpp @@ -4,6 +4,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "VulkanTestDevice.hpp" #include "../AllocationFailure.hpp" +#include "../SlabReturn.hpp" #include #include #include @@ -543,7 +544,7 @@ void ExactSizes(const Context& context) { } -int main() { +int main(int argc, char** argv) { try { std::unique_ptr device; try { @@ -553,7 +554,27 @@ int main() { std::printf("skipped, no usable Vulkan device: %s\n", error.what()); return VulkanTestSkipped; } - SlabMetadataFailures(device->GetContext()); + const auto& context = device->GetContext(); + const bool onlyReturns = argc == 2; + const bool hostOnly = onlyReturns && std::strcmp(argv[1], "--slab-return-host") == 0; + Require(!onlyReturns || hostOnly || std::strcmp(argv[1], "--slab-return-device") == 0, "unknown slab return test mode"); + bool host = false; + bool local = false; + for (std::uint32_t type = 0; type < context.memory.memoryTypeCount; ++type) { + const auto flags = context.memory.memoryTypes[type].propertyFlags; + const bool isHost = (flags & HostMemory) == HostMemory; + const bool isLocal = (flags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) != 0 && (flags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) == 0; + if ((!host && isHost && (!onlyReturns || hostOnly)) || (!local && isLocal && (!onlyReturns || !hostOnly))) { + const auto before = LiveBytes; + AgcDriver::Tests::SlabReturnWithoutAllocation(context, type); + Require(LiveBytes == before && AllocationErrors == 0, "slab returns leaked or invalidly freed native Vulkan memory"); + host |= isHost; + local |= isLocal; + } + } + Require(onlyReturns ? (hostOnly ? host : local) : host, "no requested memory type for the slab return test"); + if (onlyReturns) return 0; + SlabMetadataFailures(context); RobustReadPastEnd(device->GetContext()); DeviceBindingFailures(device->GetContext()); ExactSizes(device->GetContext()); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp index 1242c30e33..56ea345d1f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp @@ -7,6 +7,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "Optimization/ResourceProgram.hpp" #include "CompiledVariant.hpp" +#include "Optimization/RequestMemoryView.hpp" #include #include #include @@ -481,6 +482,197 @@ void RetainedPixelMatchesNullProgram(AgcDriver::VulkanDevice& device) { } } +void CheckMergedHull(AgcDriver::VulkanDevice& device) { + using namespace AgcDriver::DriverDetail; + using namespace ShaderRecompiler; + Fixture merged; + Fixture domain; + Fixture fragment; + merged.Initialize(3u); + domain.Initialize(2u); + fragment.Initialize(1u); + ShaderRegistry registry; + for (const auto* fixture : {&merged, &domain, &fragment}) registry.emplace(fixture->snapshot->codeAddress, std::vector>{fixture->snapshot}); + AgcDriver::QueueState queue{}; + queue.context[0x8e] = 0xfu; + queue.context[0x8f] = 0xfu; + merged.Bind(queue, 0x148u, 0x10bu); + domain.Bind(queue, 0xc8u, 0x8bu); + fragment.Bind(queue, 0x8u, 0xbu); + const auto unset = queue; + const auto null = [&] { + auto state = queue; + state.shader[0x102u] = 0u; + state.shader[0x103u] = 0u; + return state; + }(); + alignas(8) const std::array pointer{}; + const auto pointerAddress = reinterpret_cast(pointer.data()); + queue.shader[0x102u] = static_cast(pointerAddress); + queue.shader[0x103u] = static_cast(pointerAddress >> 32u); + DrawDecode prepared{}; + prepared.state.stages.path = AgcDriver::Graphics::ShaderPath::Tessellation; + prepared.state.stages.vertexWaveSize = 64u; + prepared.state.stages.fragmentWaveSize = 32u; + prepared.state.stages.tessellation = TessellationConfiguration{3, 4, 1, 2, 2}; + prepared.pixel.wave32 = true; + prepared.pixel.targetOutputMode[0] = 9; + prepared.pixel.targetExportMapping.fill(0xe4u); + DecodeGraphicsPrograms(prepared, queue, registry, true, true); + DrawDecode draw{}; + draw.state = prepared.state; + draw.state.stages.tessellation->mergedHull = false; + draw.pixel = prepared.pixel; + DecodeGraphicsPrograms(draw, queue, registry, false, true); + for (const auto* decoded : {&prepared, &draw}) { + Require(decoded->state.stages.tessellation->mergedHull, "a registered type-3 hull program was not decoded as a merged hull"); + Require(decoded->roles == std::vector{ProgramRole::Local, ProgramRole::Hull, ProgramRole::Domain, ProgramRole::Fragment}, "a merged hull program did not give the local and hull roles"); + const auto& local = decoded->programs[0]; + const auto& hull = decoded->programs[1]; + Require(local.snapshot == merged.snapshot && hull.snapshot == merged.snapshot && local.codeOffset == 64u && hull.codeOffset == 64u, "the local and hull stages of a merged hull program refer to another program"); + Require(local.binary.stage == ShaderStage::Local && hull.binary.stage == ShaderStage::TessellationControl && local.firstUserSgpr == 8u && hull.firstUserSgpr == 0u && hull.userData.size() == local.userData.size() + 8u, "a merged hull program's stages lost the merged user SGPR layout"); + } + Require(draw.programs[1].userData[0] == static_cast(pointerAddress) && draw.programs[1].userData[1] == static_cast(pointerAddress >> 32u), "a merged hull draw did not pass its user-data address"); + Require(UserDataWords(draw.programs, draw.roles, 0x10eu) == std::vector>{{0u, 2u}, {1u, 10u}}, "an indirect draw's user-data register does not reach both stages of a merged hull program"); + std::vector programMemory; + AppendProgramMemory(programMemory, draw.programs); + Require(programMemory.size() == 6u, "a merged hull draw's program memory does not list each code and header region once"); + try { + const RequestMemoryView view(programMemory); + } catch (const std::exception& error) { + throw std::runtime_error(std::string("a merged hull draw's program memory is not a valid request memory: ") + error.what()); + } + for (const auto* state : {&unset, &null}) { + DrawDecode without{}; + without.state = prepared.state; + without.pixel = prepared.pixel; + DecodeGraphicsPrograms(without, *state, registry, false, true); + Require(without.roles == draw.roles && without.programs[1].userData.size() == draw.programs[1].userData.size() && without.programs[1].userData[0] == 0u && without.programs[1].userData[1] == 0u, "a merged hull draw without a user-data address was not decoded with zero s[0:1]"); + } + auto unmapped = queue; + unmapped.shader[0x102u] = 0x10u; + unmapped.shader[0x103u] = 0u; + DrawDecode stale{}; + stale.state = prepared.state; + stale.pixel = prepared.pixel; + DecodeGraphicsPrograms(stale, unmapped, registry, false, true); + Require(stale.programs[1].userData[0] == 0x10u && stale.programs[1].userData[1] == 0u, "a merged hull draw did not pass a user-data address it does not need unchecked"); + const auto stages = PrepareGraphicsStages(prepared, device.Target()); + Require(stages.size() == 4u && stages[0].entry.handle != stages[1].entry.handle, "a merged hull program was not prepared as separate local and hull stages"); + for (const auto& stage : stages) stage.snapshot->prepared->entries.push_back(stage.entry); + std::vector linked; + std::vector memory; + AppendProgramMemory(memory, draw.programs); + for (std::size_t index = 0; index < draw.programs.size(); ++index) { + const auto& program = draw.programs[index]; + linked.push_back({draw.roles[index], program.binary, program.userDataBase, program.firstUserSgpr, program.userData}); + } + const auto predicted = merged.snapshot->prepared->entries.size(); + for (std::size_t index = 0; index < 2u; ++index) { + const auto& program = draw.programs[index]; + const auto vertex = AgcDriver::Graphics::DecodeVertexStageInfo(program.binary.header, program.binary.headerAddress, program.userData, nullptr, true); + RecompileRequest request{program.binary, {64u, program.firstUserSgpr, program.userData, {}, std::nullopt, vertex, memory}, device.Target(), {0, 0, 0, 128u}, GraphicsCompileContext{program.firstUserSgpr, linked, std::nullopt, draw.state.stages.tessellation, {0, 3, 4, 1}}}; + static_cast(InvocationFor(*program.snapshot, program.codeOffset, request)); + Require(merged.snapshot->prepared->entries.size() == predicted, "a merged hull stage prepared at ABI resolution was prepared again at its draw"); + request.graphics->tessellation->mergedHull = false; + Require(!MatchesPreparedShader(request, *stages[index].entry.handle), "a separate hull configuration matched a merged hull artifact"); + } + Fixture front; + front.Initialize(5u); + registry.emplace(front.snapshot->codeAddress, std::vector>{front.snapshot}); + Fixture back; + back.Initialize(7u); + registry.emplace(back.snapshot->codeAddress, std::vector>{back.snapshot}); + auto separate = queue; + front.Bind(separate, 0x148u, 0x10bu); + back.Bind(separate, 0x108u, 0x10bu); + DrawDecode halves{}; + halves.state = prepared.state; + halves.pixel = prepared.pixel; + DecodeGraphicsPrograms(halves, separate, registry, false, true); + Require(!halves.state.stages.tessellation->mergedHull && halves.programs[0].snapshot == front.snapshot && halves.programs[1].snapshot == back.snapshot, "separate hull halves were decoded as a merged hull program"); + auto halvesUnset = unset; + front.Bind(halvesUnset, 0x148u, 0x10bu); + back.Bind(halvesUnset, 0x108u, 0x10bu); + DrawDecode halvesWithout{}; + halvesWithout.state = prepared.state; + halvesWithout.pixel = prepared.pixel; + Reject([&] { DecodeGraphicsPrograms(halvesWithout, halvesUnset, registry, false, true); }, "merged shader user-data address is null"); +} + +void CheckMergedHullVertexTables(AgcDriver::VulkanDevice& device) { + using namespace AgcDriver::DriverDetail; + using namespace ShaderRecompiler; + struct Header { + Shader shader{}; + ShaderUserData users{}; + std::array direct{}; + ShaderSemantic semantic{}; + } header; + alignas(256) std::array code{}; + code.fill(0xffffffffu); + code.back() = 0xbf810000u; + header.shader.type = 3; + header.shader.user_data = &header.users; + header.direct.fill(0xffffu); + header.direct[8] = 10u; + header.direct[10] = 12u; + header.users.direct_resource_offset = header.direct.data(); + header.users.direct_resource_count = static_cast(header.direct.size()); + header.semantic.size_in_elements = 4u; + header.shader.input_semantics = &header.semantic; + header.shader.num_input_semantics = 1u; + auto merged = std::make_shared(); + merged->type = 3; + merged->codeAddress = reinterpret_cast(code.data()); + merged->headerAddress = reinterpret_cast(&header); + merged->code.assign(code.begin(), code.end()); + merged->header.resize(sizeof(header)); + std::memcpy(merged->header.data(), &header, sizeof(header)); + Fixture domain; + Fixture fragment; + domain.Initialize(2u); + fragment.Initialize(1u); + ShaderRegistry registry; + registry.emplace(merged->codeAddress, std::vector>{merged}); + for (const auto* fixture : {&domain, &fragment}) registry.emplace(fixture->snapshot->codeAddress, std::vector>{fixture->snapshot}); + AgcDriver::QueueState queue{}; + queue.context[0x8e] = 0xfu; + queue.context[0x8f] = 0xfu; + const auto entry = merged->codeAddress + 256u; + queue.shader[0x148u] = static_cast(entry >> 8u); + queue.shader[0x149u] = static_cast(entry >> 40u); + queue.shader[0x10bu] = 16u << 1u; + domain.Bind(queue, 0xc8u, 0x8bu); + fragment.Bind(queue, 0x8u, 0xbu); + alignas(16) const std::array bufferTable{}; + alignas(16) const std::array attributeTable{}; + const auto bufferAddress = reinterpret_cast(bufferTable.data()); + const auto attributeAddress = reinterpret_cast(attributeTable.data()); + for (std::uint32_t slot = 2u; slot < 6u; ++slot) queue.shader[0x10cu + slot] = slot % 2u == 0u ? 0x12345600u : 0x00000077u; + queue.shader[0x10cu + 10u] = static_cast(bufferAddress); + queue.shader[0x10cu + 11u] = static_cast(bufferAddress >> 32u); + queue.shader[0x10cu + 12u] = static_cast(attributeAddress); + queue.shader[0x10cu + 13u] = static_cast(attributeAddress >> 32u); + DrawDecode draw{}; + draw.state.stages.path = AgcDriver::Graphics::ShaderPath::Tessellation; + draw.state.stages.vertexWaveSize = 64u; + draw.state.stages.fragmentWaveSize = 32u; + draw.state.stages.tessellation = TessellationConfiguration{3, 3, 1, 2, 2}; + draw.pixel.wave32 = true; + draw.pixel.targetOutputMode[0] = 9; + draw.pixel.targetExportMapping.fill(0xe4u); + DecodeGraphicsPrograms(draw, queue, registry, false, true); + Require(draw.state.stages.tessellation->mergedHull && draw.programs[1].firstUserSgpr == 0u, "the merged hull draw with vertex tables was not decoded as a merged hull"); + for (std::size_t index = 0; index < 2u; ++index) { + const auto& program = draw.programs[index]; + std::vector reads; + const auto vertex = AgcDriver::Graphics::DecodeVertexStageInfo(program.binary.header, program.binary.headerAddress, program.userData, &reads, false, 8u - program.firstUserSgpr); + Require(vertex.fetchEmbedded && vertex.fetchAttribReg == 12u && vertex.fetchBufferReg == 10u && vertex.resourcesNum == 1u, "a merged hull stage did not decode its vertex tables"); + Require(reads.size() == 2u && reads[0].address == attributeAddress && reads[1].address == bufferAddress, "a merged hull stage read its vertex tables from user-data words other than its slots"); + } +} + void NullPixelMatchesRegistration(AgcDriver::VulkanDevice& device) { using namespace AgcDriver::DriverDetail; Fixture front; @@ -541,6 +733,8 @@ int main(int argc, char** argv) { Check(*device, AgcDriver::Graphics::ShaderPath::Vertex, dump); Check(*device, AgcDriver::Graphics::ShaderPath::Geometry, dump); Check(*device, AgcDriver::Graphics::ShaderPath::Tessellation, dump); + CheckMergedHull(*device); + CheckMergedHullVertexTables(*device); std::cout << "prepared graphics ABI tests passed\n"; return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp index aaa4b364e1..734a7c6825 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp @@ -1198,6 +1198,69 @@ void AbiDeferredRectanglePairs() { ExpectFailure([] { AgcDriverShutdown_nid_postfix(); }, "custom per-vertex interpolation is unsupported"); } +void RelocatedHeaders() { + alignas(256) static const std::array code{0x7e0002ffu, 0, 0x7e0202ffu, 0, 0x7e0402ffu, 0, 0x7e0602ffu, 0x3f800000u, 0xf80008cfu, 0x03020100u, 0xbf810000u}; + struct Header { + Shader shader{}; + std::array registers{}; + ShaderSpecialRegs specials{}; + ShaderUserData users{}; + std::array context{{{0x2d5, 0x2000}}}; + }; + static Header registered; + const auto address = reinterpret_cast(code.data()); + const auto fill = [&](Header& header) { + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18; + header.shader.header_size = sizeof(Header); + header.shader.shader_size = sizeof(code); + header.shader.code = code.data(); + header.shader.sh_registers = header.registers.data(); + header.shader.num_sh_registers = header.registers.size(); + header.shader.cx_registers = header.context.data(); + header.shader.num_cx_registers = header.context.size(); + header.shader.specials = &header.specials; + header.shader.user_data = &header.users; + header.specials.dispatch_modifier = 0x8000; + header.shader.type = 2; + header.registers = {{{0xc8, static_cast(address >> 8u)}, {0xc9, static_cast(address >> 40u)}, {0x8b, 0}, {0x8a, 0}, {0xca, 0}, {0xcb, 0}, {0xcc, 0}}}; + }; + fill(registered); + const std::array primitive{{{0x242, 4}}}; + AgcDriverRegisterShader_nid_postfix(®istered.shader); + AgcDriverResolveShaderAbi_nid_postfix(®istered.shader, {}, primitive); + std::vector commands; + for (const auto reg : registered.registers) commands.insert(commands.end(), {0xc0017600u, reg.offset, reg.value}); + for (const auto reg : registered.context) commands.insert(commands.end(), {0xc0016900u, reg.offset, reg.value}); + commands.insert(commands.end(), {0xc0017600u, 0x8, 0, 0xc0016900u, 0xd, 0x00100010u, 0xc0016900u, 0x204, 0x80000u, 0xc0016900u, 0x1c3, 4, 0xc0017900u, 0x242, 4, 0xc0012d00u, 3, 2}); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + // A title that relocates a registered header and its arrays into memory of its own resolves the ABI with the copy, + // which was never registered. Every array it names holds the registered bytes, so the registered header decodes it. + static Header relocated; + fill(relocated); + AgcDriverResolveShaderAbi_nid_postfix(&relocated.shader, {}, primitive); + Require(sceAgcDriverSubmitDcb(&packet) == 0, "the relocated header's draw was not submitted"); + AgcDriverWaitIdle_nid_postfix(); + // A copy that moves only its user data pointer beside the registered arrays keeps resolving. + static Header movedUserData; + movedUserData = registered; + movedUserData.shader.user_data = &movedUserData.users; + AgcDriverResolveShaderAbi_nid_postfix(&movedUserData.shader, {}, primitive); + // A copy whose registers, specials or semantics differ is still a replaced header. + static Header patched; + fill(patched); + patched.registers[2].value ^= 2u; + ExpectFailure([&] { AgcDriverResolveShaderAbi_nid_postfix(&patched.shader, {}, primitive); }, "replaced shader header"); + static Header otherSpecials; + fill(otherSpecials); + otherSpecials.specials.dispatch_modifier = 0x4000; + ExpectFailure([&] { AgcDriverResolveShaderAbi_nid_postfix(&otherSpecials.shader, {}, primitive); }, "replaced shader header"); + static Header otherContext; + fill(otherContext); + otherContext.context[0].value = 0x2001; + ExpectFailure([&] { AgcDriverResolveShaderAbi_nid_postfix(&otherContext.shader, {}, primitive); }, "replaced shader header"); +} + } int main(int argc, char** argv) { @@ -1254,6 +1317,7 @@ int main(int argc, char** argv) { PrepareMultisampledStorage(*device); device.reset(); SharedCodeHeaders(); + RelocatedHeaders(); RegistrationWithoutSpecials(); Registration(argc == 2); std::cout << "prepared shader and transactional registration tests passed\n"; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/QueuedIndexReadOrder.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/QueuedIndexReadOrder.cpp new file mode 100644 index 0000000000..acc2b1617a --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/QueuedIndexReadOrder.cpp @@ -0,0 +1,432 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::uint32_t Width = 192; +constexpr std::uint32_t Height = 128; +constexpr std::uint32_t DistanceVector = 0x00400000u; +constexpr std::uint32_t SetContextReg = 0x69u; +constexpr std::uint32_t SetShReg = 0x76u; +constexpr std::uint32_t SetUconfigReg = 0x79u; +constexpr std::uint32_t DrawIndexOffset2Header = 0xc0033500u; + +constexpr std::size_t Block = 65536; +constexpr std::size_t PoolBytes = 2 * Block; +constexpr std::size_t ColorBytes = Width * Height * 4; +constexpr std::uint32_t FormatRgba16Float = 71; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t Standard64KB = 0x09; +constexpr std::uint32_t ImageWidth = 128; +constexpr std::uint32_t ImageHeight = 120; +constexpr std::uint8_t Sentinel = 0xc7; +constexpr std::array Indices{0, 1, 2}; +constexpr std::size_t VertexOffset = 0x2000; + +std::byte* ColorMemory = nullptr; +std::byte* Pool = nullptr; +std::byte* Copied = nullptr; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80020005, 0xe0382010, 0x80020405, 0xbf8c3f70, + 0xf80000cf, 0x03020100, 0xf80008df, 0x07060504, 0xbf810000, +}; + +alignas(256) constexpr std::array PixelCode{ + 0x7e0002f2, 0x7e0202f2, 0x7e0402f2, 0x7e0602f2, 0xf800080f, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array ImageWriteCode{ + 0xd7460004u, 0x0401060eu, 0xd7460005u, 0x0405060fu, 0x7e100d04u, 0x7e120d05u, + 0x7e140c0du, 0x7e1602f2u, 0xf0200f08u, 0x00000804u, 0xbf810000u, +}; + +struct Vertex { + std::array position; + std::array distances; +}; + +std::array Vertices{}; + +constexpr std::array PositionX{-3.0f, 1.0f, 1.0f}; +constexpr std::array PositionY{-1.0f, -1.0f, 3.0f}; +constexpr std::array Negative{-1.0f, -1.0f, -1.0f}; + +struct RegisteredShader { + Shader shader; + ShaderUserData userData; +}; +RegisteredShader VertexShader{}; +RegisteredShader PixelShader{}; + +const std::vector> ContextBaseline{ + {0x2d5, 0x2000}, + {0x1b6, 0}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, + {0x31b, 0}, {0x31d, 0}, {0x3b8, 0x9000000}, {0x1e0, 0}, + {0xc, 0}, + {0x81, 0x80000000}, {0x90, 0x80000000}, {0x94, 0x80000000}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)}, {0x114, 0}, {0x113, std::bit_cast(1.0f)}, +}; + +std::byte* AllocateWatched(std::size_t bytes) { +#ifdef _WIN32 + void* block = GuestArena::GuestArenaAllocate_nid_postfix(bytes, Block); + GuestArena::GuestArenaCommit_nid_postfix(block, bytes, PAGE_READWRITE, bytes); +#else + void* raw = mmap(nullptr, bytes + Block, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (raw == MAP_FAILED) throw std::runtime_error("queued index read: cannot map the watched block"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + Block - 1) & ~static_cast(Block - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + Block) munmap(reinterpret_cast(aligned + bytes), begin + Block - aligned); + void* block = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, bytes); +#endif + return static_cast(block); +} + +void Register(RegisteredShader& registered, const void* code, std::uint32_t bytes, std::uint8_t type) { + registered.shader.file_header = 0x34333231u; + registered.shader.version = 0x18u; + registered.shader.user_data = ®istered.userData; + registered.shader.code = code; + registered.shader.header_size = sizeof(RegisteredShader); + registered.shader.shader_size = bytes; + registered.shader.type = type; + AgcDriverRegisterShader_nid_postfix(®istered.shader); +} + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +void SetRegister(std::vector& words, std::uint32_t opcode, std::uint32_t offset, std::uint32_t value) { + words.push_back(0xc0010000u | (opcode << 8u)); + words.push_back(offset); + words.push_back(value); +} + +std::array ImageDescriptor(const void* texels) { + const auto address = reinterpret_cast(texels); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (FormatRgba16Float << 20u) | (((ImageWidth - 1u) & 3u) << 30u), + ((ImageWidth - 1u) >> 2u) | ((ImageHeight - 1u) << 14u), + 0xfacu | (Standard64KB << 20u) | (Type2D << 28u), + 0u, 0u, 0u, 0u, + }; +} + +std::vector ImageWrite() { + const auto descriptor = ImageDescriptor(Pool); + std::vector data(descriptor.begin(), descriptor.end()); + data.resize(14, 0u); + data[13] = 7u; + 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{8, 8, 1}; + registers(0x207, shape); + const auto code = reinterpret_cast(ImageWriteCode.data()); + const std::array program{static_cast(code >> 8u), static_cast(code >> 40u)}; + registers(0x20c, program); + const std::array resource{(static_cast(data.size()) << 1u) | (7u << 7u) | (1u << 11u)}; + registers(0x213, resource); + registers(0x240, data); + words.insert(words.end(), {0xc0031500u, shape[0], shape[1], shape[2], 0x41u}); + return words; +} + +std::vector Draw(const void* vertices, std::size_t count) { + const auto colorAddress = reinterpret_cast(ColorMemory); + const auto poolAddress = reinterpret_cast(Pool); + const auto vertexAddress = reinterpret_cast(VertexCode.data()); + const auto pixelAddress = reinterpret_cast(PixelCode.data()); + std::vector words; + for (const auto& [offset, value] : ContextBaseline) SetRegister(words, SetContextReg, offset, value); + SetRegister(words, SetContextReg, 0x207, DistanceVector); + SetRegister(words, SetContextReg, 0x318, static_cast(colorAddress >> 8u)); + SetRegister(words, SetContextReg, 0x390, static_cast(colorAddress >> 40u)); + SetRegister(words, SetContextReg, 0x3b0, ((Width - 1u) << 14u) | (Height - 1u)); + SetRegister(words, SetContextReg, 0x10f, std::bit_cast(Width / 2.0f)); + SetRegister(words, SetContextReg, 0x110, std::bit_cast(Width / 2.0f)); + SetRegister(words, SetContextReg, 0x111, std::bit_cast(-(Height / 2.0f))); + SetRegister(words, SetContextReg, 0x112, std::bit_cast(Height / 2.0f)); + const auto extent = (Height << 16u) | Width; + SetRegister(words, SetContextReg, 0xd, extent); + SetRegister(words, SetContextReg, 0x82, extent); + SetRegister(words, SetContextReg, 0x91, extent); + SetRegister(words, SetContextReg, 0x95, extent); + SetRegister(words, SetUconfigReg, 0x242, 4u); + SetRegister(words, SetShReg, 0x008, static_cast(pixelAddress >> 8u)); + SetRegister(words, SetShReg, 0x009, static_cast(pixelAddress >> 40u)); + SetRegister(words, SetShReg, 0x00b, 0u); + SetRegister(words, SetShReg, 0x0c8, static_cast(vertexAddress >> 8u)); + SetRegister(words, SetShReg, 0x0c9, static_cast(vertexAddress >> 40u)); + SetRegister(words, SetShReg, 0x08b, 4u << 1u); + const auto descriptor = BufferDescriptor(vertices, sizeof(Vertex), static_cast(count)); + for (std::uint32_t i = 0; i < descriptor.size(); ++i) SetRegister(words, SetShReg, 0x08c + i, descriptor[i]); + words.insert(words.end(), {0xc0012600u, static_cast(poolAddress), static_cast(poolAddress >> 32u)}); + words.insert(words.end(), {0xc0001300u, static_cast(Indices.size())}); + SetRegister(words, SetUconfigReg, 0x243, 0x401u); + words.insert(words.end(), {DrawIndexOffset2Header, static_cast(Indices.size()), 0u, static_cast(Indices.size()), 0u}); + return words; +} + +void Submit(const std::vector& words, const char* what) { + Packet packet{const_cast(words.data()), static_cast(words.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, std::string("queued index read: the submission failed for ") + what); +} + +std::vector CopyPool(std::size_t bytes) { + const auto source = reinterpret_cast(Pool); + const auto destination = reinterpret_cast(Copied); + return {0xc0055000u, 0u, static_cast(source), static_cast(source >> 32u), static_cast(destination), static_cast(destination >> 32u), static_cast(bytes)}; +} + +void RequireCopiedImage(std::size_t bytes, const char* phase) { + std::size_t differing = 0; + for (std::size_t i = 0; i < bytes; ++i) differing += Copied[i] != Pool[i] ? 1u : 0u; + Require(differing == 0, std::string("queued index read: ") + phase + ": " + std::to_string(differing) + " of " + std::to_string(bytes) + " copied bytes miss the image's results over the copy source"); +} + +std::vector WaitRegMem(std::uint64_t address, std::uint32_t reference) { + return {0xc0053c00u, 0x13u, static_cast(address), static_cast(address >> 32u), reference, 0xffffffffu, 0u}; +} + +bool IndicesIntact() { + return std::equal(Indices.begin(), Indices.end(), reinterpret_cast(Pool)); +} + +bool VerticesIntact() { + return std::memcmp(Pool + VertexOffset, Vertices.data(), sizeof(Vertex) * Vertices.size()) == 0; +} + +std::uint32_t PoolWord(std::size_t offset) { + return *reinterpret_cast(Pool + offset); +} + +void ResetPool() { + std::memset(Pool, Sentinel, PoolBytes); + std::memcpy(Pool, Indices.data(), Indices.size() * sizeof(std::uint32_t)); + std::memcpy(Pool + VertexOffset, Vertices.data(), sizeof(Vertex) * Vertices.size()); +} + +std::size_t WrittenPixels() { + std::vector colors(ColorBytes); + AgcDriver::GuestMemory::Read(reinterpret_cast(reinterpret_cast(ColorMemory)), colors); + std::size_t written = 0; + for (std::uint32_t pixel = 0; pixel < Width * Height; ++pixel) { + const auto value = std::to_integer(colors[pixel * 4u]); + Require(value == 0u || value == 255u, "queued index read: pixel " + std::to_string(pixel) + " stored " + std::to_string(value)); + written += value == 255u ? 1u : 0u; + } + return written; +} + +void RequireTriangle(const char* phase) { + const auto written = WrittenPixels(); + Require(written == Width * Height, std::string("queued index read: ") + phase + ": the draw left " + std::to_string(Width * Height - written) + " pixels cleared, expected the triangle its indices and vertices name"); +} + +void LandPool() { + const auto landBy = std::chrono::steady_clock::now() + std::chrono::seconds(5); + do { + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(reinterpret_cast(Pool)), PoolBytes); + AgcDriverWaitIdle_nid_postfix(); + } while (IndicesIntact() && std::chrono::steady_clock::now() < landBy); + Require(!IndicesIntact(), "queued index read: the image's results never reached the memory they cover, so the run above proves nothing"); +} + +void WaitForWord(std::uint64_t address, std::uint32_t value, const char* what) { + const auto start = std::chrono::steady_clock::now(); + const auto deadline = start + std::chrono::seconds(10); + while (*reinterpret_cast(address) != value) { + if (std::chrono::steady_clock::now() >= deadline) { + char message[256]; + std::snprintf(message, sizeof(message), "queued index read: %s: 0x%08llx held 0x%08x after %.1f s, expected 0x%08x", what, static_cast(address), *reinterpret_cast(address), std::chrono::duration(std::chrono::steady_clock::now() - start).count(), value); + Require(false, message); + } + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } +} + +std::atomic GateOpen{true}; + +void GateUntilQueued() { + AgcDriver::DriverDetail::DrawPipeline::Queue0().Enqueue([] { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (AgcDriver::DriverDetail::DrawPipeline::QueuedItems() < 2 || !GateOpen.load()) { + if (std::chrono::steady_clock::now() >= deadline) return; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + }, {}); +} + +void WaitForCommits() { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (AgcDriver::DriverDetail::DrawPipeline::QueuedItems() != 0) { + Require(std::chrono::steady_clock::now() < deadline, "queued index read: the draw commit never ran"); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } +} + +void WaitForHeldRange(std::uint64_t address, std::size_t bytes, const char* what) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (!AgcDriver::DriverDetail::DrawPipeline::HeldReads(address, bytes) && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + const bool held = AgcDriver::DriverDetail::DrawPipeline::HeldReads(address, bytes); + GateOpen = true; + Require(held, std::string("queued index read: the queued draw holds no read of its ") + what + " range"); +} + +void CopyPendingImage(std::size_t bytes, const char* phase) { + const auto poolAddress = reinterpret_cast(reinterpret_cast(Pool)); + ResetPool(); + std::memset(Copied, 0, PoolBytes); + Submit(ImageWrite(), phase); + AgcDriverWaitIdle_nid_postfix(); + Require(AgcDriver::Graphics::StorageTexture::AnyPending(), std::string("queued index read: ") + phase + ": the image write left no pending result to order against"); + GateOpen = false; + GateUntilQueued(); + Submit(CopyPool(bytes), phase); + WaitForHeldRange(poolAddress, bytes, "copy source"); + WaitForCommits(); + AgcDriverWaitIdle_nid_postfix(); + LandPool(); + RequireCopiedImage(bytes, phase); +} + +} + +int main() { +#ifdef _WIN32 + _putenv_s("APS5_PIPELINED_DRAWS", "1"); + _putenv_s("APS5_PIPELINE_DMA", "1"); +#else + setenv("APS5_PIPELINED_DRAWS", "1", 1); + setenv("APS5_PIPELINE_DMA", "1", 1); +#endif + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + ColorMemory = AllocateWatched(2 * Block); + Pool = AllocateWatched(PoolBytes); + Copied = AllocateWatched(PoolBytes); + GuestAllocations::Mutation().Add(Pool, PoolBytes, true, true, true); + GuestAllocations::Mutation().Add(Copied, PoolBytes, true, true, true); + Register(VertexShader, VertexCode.data(), sizeof(VertexCode), 2); + Register(PixelShader, PixelCode.data(), sizeof(PixelCode), 1); + std::memset(Pool, Sentinel, PoolBytes); + std::memcpy(Pool, Indices.data(), Indices.size() * sizeof(std::uint32_t)); + for (std::size_t i = 0; i < Vertices.size(); ++i) Vertices[i] = {{PositionX[i], PositionY[i], 0.5f, 1.0f}, {Negative[i], Negative[i], Negative[i], 0.0f}}; + std::memcpy(Pool + VertexOffset, Vertices.data(), sizeof(Vertex) * Vertices.size()); + + const auto poolAddress = reinterpret_cast(reinterpret_cast(Pool)); + const auto vertexAddress = poolAddress + VertexOffset; + const std::array, 1> poolRange{{ + {poolAddress, static_cast(PoolBytes)}}}; + Submit(ImageWrite(), "the image write"); + AgcDriverWaitIdle_nid_postfix(); + Require(AgcDriver::Graphics::StorageTexture::AnyPending(), "queued index read: the image write left no pending result to order against"); + Require(IndicesIntact(), "queued index read: the image's results reached guest memory before any consumer asked"); + GateUntilQueued(); + std::fill_n(ColorMemory, ColorBytes, std::byte{0}); + Submit(Draw(Vertices.data(), Vertices.size()), "the indexed draw"); + WaitForCommits(); + Require(IndicesIntact(), "queued index read: the queued draw's commit stored the image's results over its index buffer"); + AgcDriverWaitIdle_nid_postfix(); + RequireTriangle("the index read"); + LandPool(); + + ResetPool(); + Submit(ImageWrite(), "the second image write"); + AgcDriverWaitIdle_nid_postfix(); + Require(AgcDriver::Graphics::StorageTexture::AnyPending(), "queued index read: the second image write left no pending result to order against"); + GateOpen = false; + GateUntilQueued(); + std::fill_n(ColorMemory, ColorBytes, std::byte{0}); + Submit(Draw(Pool + VertexOffset, Vertices.size()), "the draw whose vertices the image covers"); + WaitForHeldRange(vertexAddress, sizeof(Vertex) * Vertices.size(), "vertex"); + WaitForCommits(); + AgcDriverWaitIdle_nid_postfix(); + RequireTriangle("the vertex read"); + LandPool(); + + std::array texels{}; + for (std::size_t i = 0; i < texels.size(); ++i) texels[i] = PoolWord(i * sizeof(std::uint32_t)); + const auto sentinelWord = static_cast(Sentinel) * 0x01010101u; + std::size_t awaited = texels.size(); + for (std::size_t i = 0; i < texels.size(); ++i) { + if (texels[i] != Indices[i] && texels[i] != sentinelWord) { + awaited = i; + break; + } + } + Require(awaited != texels.size(), "queued index read: the image's first texels repeat the indices and the sentinel, so a wait on them proves nothing"); + const auto awaitedAddress = poolAddress + awaited * sizeof(std::uint32_t); + ResetPool(); + + Submit(ImageWrite(), "the third image write"); + AgcDriverWaitIdle_nid_postfix(); + Require(AgcDriver::Graphics::StorageTexture::AnyPending(), "queued index read: the third image write left no pending result to order against"); + std::fill_n(ColorMemory, ColorBytes, std::byte{0}); + Submit(Draw(Pool + VertexOffset, Vertices.size()), "the draw a wait behind it reads the held range of"); + Submit(WaitRegMem(awaitedAddress, texels[awaited]), "the wait over the held range"); + WaitForWord(awaitedAddress, texels[awaited], "the wait over the held range was not satisfied"); + AgcDriverWaitIdle_nid_postfix(); + RequireTriangle("the wait over the held range"); + Require(!VerticesIntact(), "queued index read: the image's results never reached the memory the wait covered"); + CopyPendingImage(4096, "the queued copy of the image"); + std::puts("queued index read vs. deferred storage write-back tests passed"); + AgcDriverShutdown_nid_postfix(); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} 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/RegistrationPrepareStack.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/RegistrationPrepareStack.cpp new file mode 100644 index 0000000000..20c3fd9389 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/RegistrationPrepareStack.cpp @@ -0,0 +1,112 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif + +namespace { + +constexpr std::uint64_t PoolStackBytes = 64ull << 20u; +constexpr std::uint64_t MostCommittedBytes = 8ull << 20u; + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error("registration prepare stack: " + message); +} + +struct StackUse { + std::thread::id thread; + std::uint64_t reserved = 0; + std::uint64_t committed = 0; +}; + +StackUse MeasureCurrentStack() { + StackUse use; + use.thread = std::this_thread::get_id(); +#ifdef _WIN32 + MEMORY_BASIC_INFORMATION here{}; + const int local = 0; + Require(VirtualQuery(&local, &here, sizeof(here)) != 0, "VirtualQuery of the stack failed"); + auto cursor = reinterpret_cast(here.AllocationBase); + for (;;) { + MEMORY_BASIC_INFORMATION info{}; + if (VirtualQuery(reinterpret_cast(cursor), &info, sizeof(info)) == 0 || info.AllocationBase != here.AllocationBase) break; + use.reserved += info.RegionSize; + if (info.State == MEM_COMMIT) use.committed += info.RegionSize; + cursor = reinterpret_cast(info.BaseAddress) + info.RegionSize; + } +#elif defined(__APPLE__) + use.reserved = pthread_get_stacksize_np(pthread_self()); +#else + pthread_attr_t attributes; + Require(pthread_getattr_np(pthread_self(), &attributes) == 0, "pthread_getattr_np failed"); + void* base = nullptr; + std::size_t size = 0; + pthread_attr_getstack(&attributes, &base, &size); + pthread_attr_destroy(&attributes); + use.reserved = size; +#endif + return use; +} + +StackUse MeasureOn(const std::function)>& submit) { + std::mutex mutex; + std::condition_variable done; + bool measured = false; + StackUse use; + submit([&] { + const auto result = MeasureCurrentStack(); + std::lock_guard lock(mutex); + use = result; + measured = true; + done.notify_all(); + }); + std::unique_lock lock(mutex); + done.wait(lock, [&] { return measured; }); + return use; +} + +void CheckPool(const char* name, const std::function)>& submit) { + const auto use = MeasureOn(submit); + Require(use.thread != std::this_thread::get_id(), std::string(name) + " ran the task on the caller's thread"); + Require(use.reserved >= PoolStackBytes, std::string(name) + " thread reserves " + std::to_string(use.reserved >> 20u) + " MiB of stack, less than 64 MiB"); +#ifdef _WIN32 + Require(use.committed <= MostCommittedBytes, std::string(name) + " thread commits " + std::to_string(use.committed >> 20u) + " MiB of its " + std::to_string(use.reserved >> 20u) + " MiB stack up front"); +#endif +} + +} + +int main() { + try { + AgcDriver::DriverDetail::Driver::Get(); + CheckPool("registration preparation", [](std::function task) { AgcDriver::DriverDetail::SubmitRegistrationPreparation(std::move(task)); }); + CheckPool("library prebuild", [](std::function task) { AgcDriver::DriverDetail::SubmitLibraryPrebuild(std::move(task)); }); + const char* prebuildThreads = std::getenv("APS5_PREBUILD_THREADS"); + const auto started = std::max(2u, std::thread::hardware_concurrency() / 2u) + (prebuildThreads != nullptr ? std::max(1u, static_cast(std::strtoul(prebuildThreads, nullptr, 10))) : 1u); + const auto live = AgcDriver::DriverDetail::RegistrationPreparationThreads(); + Require(live == started, "the preparation and prebuild pools count " + std::to_string(live) + " live threads of the " + std::to_string(started) + " they started"); + AgcDriver::DriverDetail::CloseRegistrationPreparation(); + Require(AgcDriver::DriverDetail::RegistrationPreparationThreads() == 0, "closing the preparation and prebuild pools left " + std::to_string(AgcDriver::DriverDetail::RegistrationPreparationThreads()) + " live threads"); + std::puts("registration prepare stack tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampleOrder.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampleOrder.cpp new file mode 100644 index 0000000000..9e534056ab --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampleOrder.cpp @@ -0,0 +1,1528 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/SampleOrder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include "SampleOrderTestFillDepth_spv.h" +#include "SampleOrderTestFillStencil_spv.h" +#include "SampleOrderTestFillVertex_spv.h" +#include "SampleOrderTestR16_spv.h" +#include "SampleOrderTestR32_spv.h" +#include "SampleOrderTestR8_spv.h" +#include "SampleOrderTestReadColor_spv.h" +#include "SampleOrderTestReadDepth_spv.h" +#include "SampleOrderTestReadStencil_spv.h" +#include "SampleOrderTestRG32_spv.h" +#include "SampleOrderTestRGBA32_spv.h" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using AgcDriver::Graphics::Check; +using AgcDriver::Graphics::Context; +using AgcDriver::Graphics::KeptSampleOrder; +using AgcDriver::Graphics::Require; +using AgcDriver::Graphics::SampleOrder; +using AgcDriver::Graphics::SampleOrderMethod; +using AgcDriver::Graphics::SampleOrderPass; +using AgcDriver::Graphics::SampleOrderTarget; +using ShaderRecompiler::PixelInput; +using ShaderRecompiler::PixelInputBit; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t WaveSize = 64; +constexpr VkImageUsageFlags DepthUsage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; +constexpr VkImageUsageFlags ColorUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_STORAGE_BIT; + +SampleOrder OrderFor(std::uint32_t samples) { + if (samples == 2u) return {KeptSampleOrder, KeptSampleOrder, 0x76543201u, 0x76543201u}; + if (samples == 4u) return {KeptSampleOrder, 0x76542301u, 0x76540132u, 0x76541023u}; + return {0x01234567u, KeptSampleOrder, 0x10325476u, 0x45670123u}; +} + +std::uint32_t Pick(std::uint32_t word, std::uint32_t index) { + return (word >> (index * 4u)) & 7u; +} + +std::uint32_t Quad(std::uint32_t x, std::uint32_t y) { + return (x & 1u) | ((y & 1u) << 1u); +} + +std::string At(std::uint32_t x, std::uint32_t y, std::uint32_t sample) { + return "sample " + std::to_string(sample) + " of pixel (" + std::to_string(x) + ", " + std::to_string(y) + ")"; +} + +void FullBarrier(const Context& context, VkCommandBuffer commands) { + AgcDriver::Graphics::RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT); +} + +void HostBarrier(const Context& context, VkCommandBuffer commands) { + AgcDriver::Graphics::RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); +} + +class Image { +public: + Image(const Context& context, VkFormat format, std::uint32_t samples, VkExtent2D extent, VkImageUsageFlags usage, VkImageCreateFlags flags) : context(context) { + try { + VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + info.flags = flags; + info.imageType = VK_IMAGE_TYPE_2D; + info.format = format; + info.extent = {extent.width, extent.height, 1u}; + info.mipLevels = 1u; + info.arrayLayers = 1u; + info.samples = static_cast(samples); + info.tiling = VK_IMAGE_TILING_OPTIMAL; + info.usage = usage; + info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + Check(context.Function("vkCreateImage")(context.device, &info, nullptr, &image), "vkCreateImage sample order test"); + VkMemoryRequirements requirements{}; + context.Function("vkGetImageMemoryRequirements")(context.device, image, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; + allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory sample order test"); + Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0u), "vkBindImageMemory sample order test"); + } catch (...) { + release(); + throw; + } + } + ~Image() { release(); } + Image(const Image&) = delete; + Image& operator=(const Image&) = delete; + VkImage Handle() const { return image; } + VkImageView View(VkFormat format, VkImageAspectFlags aspect, VkImageUsageFlags usage) { + VkImageViewUsageCreateInfo viewUsage{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO}; + viewUsage.usage = usage; + VkImageViewCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + info.pNext = &viewUsage; + info.image = image; + info.viewType = VK_IMAGE_VIEW_TYPE_2D; + info.format = format; + info.subresourceRange = {aspect, 0u, 1u, 0u, 1u}; + VkImageView view = VK_NULL_HANDLE; + Check(context.Function("vkCreateImageView")(context.device, &info, nullptr, &view), "vkCreateImageView sample order test"); + views.push_back(view); + return view; + } + void ToGeneral(VkCommandBuffer commands, VkImageAspectFlags aspect) const { + VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + barrier.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT; + barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + barrier.newLayout = VK_IMAGE_LAYOUT_GENERAL; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = image; + barrier.subresourceRange = {aspect, 0u, 1u, 0u, 1u}; + context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0u, 0u, nullptr, 0u, nullptr, 1u, &barrier); + } + +private: + void release() noexcept { + for (const auto view : views) context.Function("vkDestroyImageView")(context.device, view, nullptr); + if (image) context.Function("vkDestroyImage")(context.device, image, nullptr); + if (memory) context.Function("vkFreeMemory")(context.device, memory, nullptr); + } + Context context; + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + std::vector views; +}; + +struct Binding { + std::uint32_t binding; + VkDescriptorType type; +}; + +class Program { +public: + Program(const Context& context, std::span compute, std::span bindings, std::uint32_t pushBytes, VkShaderStageFlags stages = VK_SHADER_STAGE_COMPUTE_BIT) : context(context), stages(stages), pushBytes(pushBytes) { + try { + std::vector layoutBindings; + std::vector sizes; + for (const auto& binding : bindings) { + layoutBindings.push_back({binding.binding, binding.type, 1u, stages, nullptr}); + sizes.push_back({binding.type, 1u}); + types.push_back(binding); + } + VkDescriptorSetLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; + layoutInfo.bindingCount = static_cast(layoutBindings.size()); + layoutInfo.pBindings = layoutBindings.data(); + Check(context.Function("vkCreateDescriptorSetLayout")(context.device, &layoutInfo, nullptr, &setLayout), "sample order test descriptor layout"); + VkDescriptorPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; + poolInfo.maxSets = 1u; + poolInfo.poolSizeCount = static_cast(sizes.size()); + poolInfo.pPoolSizes = sizes.data(); + Check(context.Function("vkCreateDescriptorPool")(context.device, &poolInfo, nullptr, &pool), "sample order test descriptor pool"); + const VkDescriptorSetAllocateInfo allocate{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO, nullptr, pool, 1u, &setLayout}; + Check(context.Function("vkAllocateDescriptorSets")(context.device, &allocate, &set), "sample order test descriptor set"); + const VkPushConstantRange range{stages, 0u, pushBytes}; + VkPipelineLayoutCreateInfo layoutCreate{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + layoutCreate.setLayoutCount = 1u; + layoutCreate.pSetLayouts = &setLayout; + layoutCreate.pushConstantRangeCount = 1u; + layoutCreate.pPushConstantRanges = ⦥ + Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutCreate, nullptr, &layout), "sample order test pipeline layout"); + if (compute.empty()) return; + const auto shader = Module(compute); + VkComputePipelineCreateInfo info{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; + info.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, nullptr, 0u, VK_SHADER_STAGE_COMPUTE_BIT, shader, "main", nullptr}; + info.layout = layout; + const auto created = context.Function("vkCreateComputePipelines")(context.device, VK_NULL_HANDLE, 1u, &info, nullptr, &pipeline); + context.Function("vkDestroyShaderModule")(context.device, shader, nullptr); + Check(created, "sample order test compute pipeline"); + } catch (...) { + release(); + throw; + } + } + ~Program() { release(); } + Program(const Program&) = delete; + Program& operator=(const Program&) = delete; + + VkShaderModule Module(std::span code) const { + VkShaderModuleCreateInfo info{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + info.codeSize = code.size_bytes(); + info.pCode = code.data(); + VkShaderModule module = VK_NULL_HANDLE; + Check(context.Function("vkCreateShaderModule")(context.device, &info, nullptr, &module), "sample order test shader module"); + return module; + } + void Image(std::uint32_t binding, VkImageView view) const { + const VkDescriptorImageInfo image{VK_NULL_HANDLE, view, VK_IMAGE_LAYOUT_GENERAL}; + VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; + write.dstSet = set; + write.dstBinding = binding; + write.descriptorCount = 1u; + write.descriptorType = typeOf(binding); + write.pImageInfo = ℑ + context.Function("vkUpdateDescriptorSets")(context.device, 1u, &write, 0u, nullptr); + } + void Storage(std::uint32_t binding, VkBuffer buffer) const { + const VkDescriptorBufferInfo info{buffer, 0u, VK_WHOLE_SIZE}; + VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; + write.dstSet = set; + write.dstBinding = binding; + write.descriptorCount = 1u; + write.descriptorType = typeOf(binding); + write.pBufferInfo = &info; + context.Function("vkUpdateDescriptorSets")(context.device, 1u, &write, 0u, nullptr); + } + void Bind(VkCommandBuffer commands, VkPipelineBindPoint point, const void* push) const { + context.Function("vkCmdBindDescriptorSets")(commands, point, layout, 0u, 1u, &set, 0u, nullptr); + context.Function("vkCmdPushConstants")(commands, layout, stages, 0u, pushBytes, push); + } + void Dispatch(VkCommandBuffer commands, const void* push, VkExtent2D extent) const { + context.Function("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline); + Bind(commands, VK_PIPELINE_BIND_POINT_COMPUTE, push); + context.Function("vkCmdDispatch")(commands, (extent.width + 7u) / 8u, (extent.height + 7u) / 8u, 1u); + } + VkPipelineLayout Layout() const { return layout; } + +private: + VkDescriptorType typeOf(std::uint32_t binding) const { + for (const auto& known : types) { + if (known.binding == binding) return known.type; + } + throw std::runtime_error("the sample order test program has no binding " + std::to_string(binding)); + } + void release() noexcept { + if (pipeline) context.Function("vkDestroyPipeline")(context.device, pipeline, nullptr); + if (layout) context.Function("vkDestroyPipelineLayout")(context.device, layout, nullptr); + if (pool) context.Function("vkDestroyDescriptorPool")(context.device, pool, nullptr); + if (setLayout) context.Function("vkDestroyDescriptorSetLayout")(context.device, setLayout, nullptr); + } + Context context; + VkShaderStageFlags stages; + std::uint32_t pushBytes; + std::vector types; + VkDescriptorSetLayout setLayout = VK_NULL_HANDLE; + VkDescriptorPool pool = VK_NULL_HANDLE; + VkDescriptorSet set = VK_NULL_HANDLE; + VkPipelineLayout layout = VK_NULL_HANDLE; + VkPipeline pipeline = VK_NULL_HANDLE; +}; + +class DepthFill { +public: + DepthFill(const Context& context, VkFormat format, std::uint32_t samples, VkExtent2D extent, VkImageView attachment, bool stencil, VkBuffer values) : context(context), extent(extent), samples(samples) { + const std::array bindings{{{0u, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER}}}; + program = std::make_unique(context, std::span{}, bindings, 8u, VK_SHADER_STAGE_FRAGMENT_BIT); + program->Storage(0u, values); + try { + const bool hasStencil = AgcDriver::Graphics::HasStencil(format); + VkAttachmentDescription description{}; + description.format = format; + description.samples = static_cast(samples); + description.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + description.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + description.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + description.stencilStoreOp = hasStencil ? VK_ATTACHMENT_STORE_OP_STORE : VK_ATTACHMENT_STORE_OP_DONT_CARE; + description.initialLayout = VK_IMAGE_LAYOUT_GENERAL; + description.finalLayout = VK_IMAGE_LAYOUT_GENERAL; + const VkAttachmentReference reference{0u, VK_IMAGE_LAYOUT_GENERAL}; + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.pDepthStencilAttachment = &reference; + VkRenderPassCreateInfo passInfo{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; + passInfo.attachmentCount = 1u; + passInfo.pAttachments = &description; + passInfo.subpassCount = 1u; + passInfo.pSubpasses = &subpass; + Check(context.Function("vkCreateRenderPass")(context.device, &passInfo, nullptr, &pass), "sample order test fill pass"); + VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO}; + framebufferInfo.renderPass = pass; + framebufferInfo.attachmentCount = 1u; + framebufferInfo.pAttachments = &attachment; + framebufferInfo.width = extent.width; + framebufferInfo.height = extent.height; + framebufferInfo.layers = 1u; + Check(context.Function("vkCreateFramebuffer")(context.device, &framebufferInfo, nullptr, &framebuffer), "sample order test fill framebuffer"); + const auto vertex = program->Module(SAMPLE_ORDER_TEST_FILLVERTEX_SPV); + const auto fragment = stencil ? program->Module(SAMPLE_ORDER_TEST_FILLSTENCIL_SPV) : program->Module(SAMPLE_ORDER_TEST_FILLDEPTH_SPV); + const std::array stages{{ + {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, nullptr, 0u, VK_SHADER_STAGE_VERTEX_BIT, vertex, "main", nullptr}, + {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, nullptr, 0u, VK_SHADER_STAGE_FRAGMENT_BIT, fragment, "main", nullptr}, + }}; + VkPipelineVertexInputStateCreateInfo input{VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO}; + VkPipelineInputAssemblyStateCreateInfo assembly{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO}; + assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + const VkViewport viewport{0.0f, 0.0f, static_cast(extent.width), static_cast(extent.height), 0.0f, 1.0f}; + const VkRect2D scissor{{0, 0}, extent}; + VkPipelineViewportStateCreateInfo viewports{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO}; + viewports.viewportCount = 1u; + viewports.pViewports = &viewport; + viewports.scissorCount = 1u; + viewports.pScissors = &scissor; + VkPipelineRasterizationStateCreateInfo raster{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO}; + raster.polygonMode = VK_POLYGON_MODE_FILL; + raster.cullMode = VK_CULL_MODE_NONE; + raster.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + raster.lineWidth = 1.0f; + VkPipelineMultisampleStateCreateInfo multisample{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO}; + multisample.rasterizationSamples = static_cast(samples); + multisample.sampleShadingEnable = VK_TRUE; + multisample.minSampleShading = 1.0f; + VkStencilOpState replace{}; + replace.failOp = VK_STENCIL_OP_REPLACE; + replace.passOp = VK_STENCIL_OP_REPLACE; + replace.depthFailOp = VK_STENCIL_OP_REPLACE; + replace.compareOp = VK_COMPARE_OP_ALWAYS; + replace.compareMask = 0xffu; + replace.writeMask = 0xffu; + VkPipelineDepthStencilStateCreateInfo depth{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO}; + depth.depthTestEnable = VK_TRUE; + depth.depthWriteEnable = VK_TRUE; + depth.depthCompareOp = VK_COMPARE_OP_ALWAYS; + depth.stencilTestEnable = stencil ? VK_TRUE : VK_FALSE; + depth.front = replace; + depth.back = replace; + VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO}; + VkGraphicsPipelineCreateInfo info{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO}; + info.stageCount = static_cast(stages.size()); + info.pStages = stages.data(); + info.pVertexInputState = &input; + info.pInputAssemblyState = &assembly; + info.pViewportState = &viewports; + info.pRasterizationState = &raster; + info.pMultisampleState = &multisample; + info.pDepthStencilState = &depth; + info.pColorBlendState = &blend; + info.layout = program->Layout(); + info.renderPass = pass; + const auto created = context.Function("vkCreateGraphicsPipelines")(context.device, VK_NULL_HANDLE, 1u, &info, nullptr, &pipeline); + context.Function("vkDestroyShaderModule")(context.device, fragment, nullptr); + context.Function("vkDestroyShaderModule")(context.device, vertex, nullptr); + Check(created, "sample order test fill pipeline"); + } catch (...) { + release(); + throw; + } + } + ~DepthFill() { release(); } + DepthFill(const DepthFill&) = delete; + DepthFill& operator=(const DepthFill&) = delete; + void Run(VkCommandBuffer commands) const { + VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; + begin.renderPass = pass; + begin.framebuffer = framebuffer; + begin.renderArea = {{0, 0}, extent}; + context.Function("vkCmdBeginRenderPass")(commands, &begin, VK_SUBPASS_CONTENTS_INLINE); + context.Function("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); + const std::array push{extent.width, samples}; + program->Bind(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, push.data()); + context.Function("vkCmdDraw")(commands, 3u, 1u, 0u, 0u); + context.Function("vkCmdEndRenderPass")(commands); + } + +private: + void release() noexcept { + if (pipeline) context.Function("vkDestroyPipeline")(context.device, pipeline, nullptr); + if (framebuffer) context.Function("vkDestroyFramebuffer")(context.device, framebuffer, nullptr); + if (pass) context.Function("vkDestroyRenderPass")(context.device, pass, nullptr); + } + Context context; + VkExtent2D extent; + std::uint32_t samples; + std::unique_ptr program; + VkRenderPass pass = VK_NULL_HANDLE; + VkFramebuffer framebuffer = VK_NULL_HANDLE; + VkPipeline pipeline = VK_NULL_HANDLE; +}; + +using Samples = std::vector>; + +template +void Submit(const Context& context, Record record) { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + AgcDriver::Graphics::CommandBatch batch(context); + record(batch.Handle()); + batch.SubmitAndWait(); +} + +Samples ReadValues(AgcDriver::Graphics::Buffer& buffer, std::size_t count) { + buffer.Invalidate(); + Samples values(count); + std::memcpy(values.data(), buffer.Bytes().data(), count * sizeof(values[0])); + return values; +} + +void CheckOrdered(const Samples& before, const Samples& after, VkExtent2D extent, VkExtent2D region, std::uint32_t samples, const SampleOrder& order, bool toHost, const std::string& what) { + std::uint32_t moved = 0; + std::string first; + std::array wrong{}; + for (std::uint32_t y = 0; y < extent.height; ++y) { + for (std::uint32_t x = 0; x < extent.width; ++x) { + const bool inside = x < region.width && y < region.height; + const auto word = order[Quad(x, y)]; + const auto map = toHost ? word : AgcDriver::Graphics::InvertSampleOrder(word); + for (std::uint32_t sample = 0; sample < samples; ++sample) { + const auto base = (y * extent.width + x) * samples; + const auto source = inside ? Pick(map, sample) : sample; + if (after[base + sample] != before[base + source]) { + if (first.empty()) { + char held[96]; + std::snprintf(held, sizeof(held), " holds 0x%08x 0x%08x, expected 0x%08x 0x%08x (sample %u)", after[base + sample][0], after[base + sample][1], before[base + source][0], before[base + source][1], source); + first = At(x, y, sample) + held; + } + ++wrong[Quad(x, y)]; + } + if (source != sample) ++moved; + } + } + } + if (!first.empty()) { + std::string classes; + for (std::uint32_t quad = 0; quad < wrong.size(); ++quad) { + const bool kept = order[quad] == 0u || order[quad] == KeptSampleOrder; + classes += (quad == 0u ? "" : ", ") + std::to_string(wrong[quad]) + " in pixels with x " + ((quad & 1u) != 0u ? "odd" : "even") + " and y " + ((quad & 2u) != 0u ? "odd" : "even") + (kept ? " (kept)" : " (reordered)"); + } + throw std::runtime_error(what + ": " + first + "; wrong samples: " + classes); + } + Require(moved != 0, what + ": the order moved no sample"); +} + +const std::array, 5> ColorFormats{{ + {VK_FORMAT_R8_UNORM, 1u}, {VK_FORMAT_R16_SFLOAT, 2u}, {VK_FORMAT_R8G8B8A8_SRGB, 4u}, {VK_FORMAT_R16G16B16A16_SFLOAT, 8u}, {VK_FORMAT_R32G32B32A32_SFLOAT, 16u} +}}; + +std::span TexelCode(VkFormat alias) { + switch (alias) { + case VK_FORMAT_R8_UINT: return SAMPLE_ORDER_TEST_R8_SPV; + case VK_FORMAT_R16_UINT: return SAMPLE_ORDER_TEST_R16_SPV; + case VK_FORMAT_R32_UINT: return SAMPLE_ORDER_TEST_R32_SPV; + case VK_FORMAT_R32G32_UINT: return SAMPLE_ORDER_TEST_RG32_SPV; + case VK_FORMAT_R32G32B32A32_UINT: return SAMPLE_ORDER_TEST_RGBA32_SPV; + default: throw std::runtime_error("the sample order test has no texel program for this alias"); + } +} + +std::uint32_t AliasMask(std::uint32_t elementBytes, std::uint32_t component) { + if (elementBytes == 1u) return component == 0u ? 0xffu : 0u; + if (elementBytes == 2u) return component == 0u ? 0xffffu : 0u; + if (elementBytes == 4u) return component == 0u ? ~0u : 0u; + if (elementBytes == 8u) return component < 2u ? ~0u : 0u; + return ~0u; +} + +Samples Masked(Samples values, std::uint32_t elementBytes) { + for (auto& value : values) { + for (std::uint32_t component = 0; component < 4u; ++component) value[component] &= AliasMask(elementBytes, component); + } + return values; +} + +constexpr VkExtent2D SwapExtent{20u, 12u}; +constexpr VkExtent2D SwapRegion{16u, 10u}; + +bool ColorSwaps(const Context& context, VkFormat format, std::uint32_t elementBytes, std::uint32_t samples) { + const auto alias = AgcDriver::Graphics::SampleOrderStorageFormat(context, format, elementBytes, samples); + const auto name = "the " + std::to_string(samples) + "x color format " + std::to_string(format); + if (alias == VK_FORMAT_UNDEFINED) { + std::printf("%s has no %u-sample storage alias here, not swapped\n", name.c_str(), samples); + return false; + } + const auto order = OrderFor(samples); + Image image(context, format, samples, SwapExtent, ColorUsage, VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | VK_IMAGE_CREATE_EXTENDED_USAGE_BIT); + const auto view = image.View(alias, VK_IMAGE_ASPECT_COLOR_BIT, VK_IMAGE_USAGE_STORAGE_BIT); + const auto count = static_cast(SwapExtent.width) * SwapExtent.height * samples; + AgcDriver::Graphics::Buffer values(context, count * 16u, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + Samples before(count); + for (std::size_t index = 0; index < count; ++index) { + for (std::uint32_t component = 0; component < 4u; ++component) before[index][component] = static_cast(index * 0x9e3779b1u + component * 0x85ebca6bu + 0x1000u * (index % samples)) & AliasMask(elementBytes, component); + before[index][0] = (before[index][0] & ~7u & AliasMask(elementBytes, 0u)) | static_cast(index % samples); + } + std::memcpy(values.Bytes().data(), before.data(), count * sizeof(before[0])); + const std::array bindings{{{0u, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}, {1u, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER}}}; + Program texels(context, TexelCode(alias), bindings, 16u); + texels.Image(0u, view); + texels.Storage(1u, values.Handle()); + const std::array store{SwapExtent.width, SwapExtent.height, samples, 1u}; + const std::array load{SwapExtent.width, SwapExtent.height, samples, 0u}; + const SampleOrderTarget target{image.Handle(), view, alias, SwapExtent, samples, SampleOrderMethod::Storage, nullptr}; + const SampleOrderPass pass{order, SwapRegion, {}, false}; + std::vector> kept; + Submit(context, [&](VkCommandBuffer commands) { + image.ToGeneral(commands, VK_IMAGE_ASPECT_COLOR_BIT); + texels.Dispatch(commands, store.data(), SwapExtent); + FullBarrier(context, commands); + texels.Dispatch(commands, load.data(), SwapExtent); + HostBarrier(context, commands); + }); + Require(Masked(ReadValues(values, count), elementBytes) == before, name + ": the stored samples did not read back"); + Submit(context, [&](VkCommandBuffer commands) { + AgcDriver::Graphics::RecordSampleOrder(context, commands, pass, std::span(&target, 1u), true, kept); + FullBarrier(context, commands); + texels.Dispatch(commands, load.data(), SwapExtent); + HostBarrier(context, commands); + }); + CheckOrdered(before, Masked(ReadValues(values, count), elementBytes), SwapExtent, SwapRegion, samples, order, true, name + " put in the host's order"); + Submit(context, [&](VkCommandBuffer commands) { + AgcDriver::Graphics::RecordSampleOrder(context, commands, pass, std::span(&target, 1u), false, kept); + FullBarrier(context, commands); + texels.Dispatch(commands, load.data(), SwapExtent); + HostBarrier(context, commands); + }); + Require(Masked(ReadValues(values, count), elementBytes) == before, name + ": putting the samples back in the guest's order did not restore every sample bit for bit"); + std::printf("%s round-trips through its %u-byte storage alias\n", name.c_str(), elementBytes); + return true; +} + +bool DepthSampleable(const Context& context, VkFormat format, std::uint32_t samples) { + VkFormatProperties properties{}; + context.formatProperties(context.physical, format, &properties); + constexpr VkFormatFeatureFlags needed = VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT; + if ((properties.optimalTilingFeatures & needed) != needed) return false; + VkImageFormatProperties supported{}; + return context.imageFormatProperties(context.physical, format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, DepthUsage, 0u, &supported) == VK_SUCCESS && (supported.sampleCounts & samples) != 0u; +} + +std::string MethodName(SampleOrderMethod method) { + switch (method) { + case SampleOrderMethod::Depth: return "a depth pass"; + case SampleOrderMethod::DepthStencilExport: return "a depth pass exporting stencil"; + case SampleOrderMethod::DepthStencilBits: return "a depth pass and eight stencil bit passes"; + default: return "storage"; + } +} + +bool DepthSwaps(const Context& context, VkFormat format, std::uint32_t samples, SampleOrderMethod method) { + const bool stencil = method != SampleOrderMethod::Depth; + const auto name = "the " + std::to_string(samples) + "x depth format " + std::to_string(format) + " through " + MethodName(method); + if (!DepthSampleable(context, format, samples)) { + std::printf("%s: the format cannot be sampled at this sample count here, not swapped\n", name.c_str()); + return false; + } + if (stencil && !context.shaderStencilExport) { + std::printf("%s: the device has no stencil export to fill the test's stencil, not swapped\n", name.c_str()); + return false; + } + const auto order = OrderFor(samples); + const bool hasStencil = AgcDriver::Graphics::HasStencil(format); + const VkImageAspectFlags aspects = VK_IMAGE_ASPECT_DEPTH_BIT | (hasStencil ? VK_IMAGE_ASPECT_STENCIL_BIT : 0u); + Image image(context, format, samples, SwapExtent, DepthUsage, 0u); + const auto attachment = image.View(format, aspects, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT); + const auto depthView = image.View(format, VK_IMAGE_ASPECT_DEPTH_BIT, VK_IMAGE_USAGE_SAMPLED_BIT); + const auto stencilView = stencil ? image.View(format, VK_IMAGE_ASPECT_STENCIL_BIT, VK_IMAGE_USAGE_SAMPLED_BIT) : VK_NULL_HANDLE; + const auto count = static_cast(SwapExtent.width) * SwapExtent.height * samples; + AgcDriver::Graphics::Buffer fill(context, count * 8u, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + AgcDriver::Graphics::Buffer values(context, count * 16u, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + std::vector> filled(count); + const bool unorm16 = format == VK_FORMAT_D16_UNORM || format == VK_FORMAT_D16_UNORM_S8_UINT; + for (std::size_t index = 0; index < count; ++index) { + const auto key = static_cast(index + 1u); + filled[index] = {std::bit_cast(unorm16 ? static_cast(key) / 65535.0f : static_cast(key) / 4096.0f), (key * 37u) & 0xffu}; + } + std::memcpy(fill.Bytes().data(), filled.data(), count * sizeof(filled[0])); + DepthFill raster(context, format, samples, SwapExtent, attachment, stencil, fill.Handle()); + const std::array bindings{{{0u, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, {1u, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, {2u, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER}}}; + Program read(context, stencil ? std::span(SAMPLE_ORDER_TEST_READSTENCIL_SPV) : std::span(SAMPLE_ORDER_TEST_READDEPTH_SPV), bindings, 12u); + read.Image(0u, depthView); + if (stencil) read.Image(1u, stencilView); + read.Storage(2u, values.Handle()); + const std::array push{SwapExtent.width, SwapExtent.height, samples}; + const SampleOrderTarget target{image.Handle(), attachment, format, SwapExtent, samples, method, nullptr}; + const SampleOrderPass pass{order, SwapRegion, {}, false}; + std::vector> kept; + Submit(context, [&](VkCommandBuffer commands) { + image.ToGeneral(commands, aspects); + raster.Run(commands); + FullBarrier(context, commands); + read.Dispatch(commands, push.data(), SwapExtent); + HostBarrier(context, commands); + }); + const auto before = ReadValues(values, count); + for (std::size_t index = 0; index < count; ++index) { + const auto key = static_cast(index + 1u); + const auto depth = std::bit_cast(before[index][0]); + const bool depthHeld = unorm16 ? static_cast(depth * 65535.0f + 0.5f) == key : before[index][0] == filled[index][0]; + Require(depthHeld && (!stencil || before[index][1] == filled[index][1]), name + ": the filled sample " + std::to_string(index) + " did not read back"); + } + Submit(context, [&](VkCommandBuffer commands) { + AgcDriver::Graphics::RecordSampleOrder(context, commands, pass, std::span(&target, 1u), true, kept); + FullBarrier(context, commands); + read.Dispatch(commands, push.data(), SwapExtent); + HostBarrier(context, commands); + }); + CheckOrdered(before, ReadValues(values, count), SwapExtent, SwapRegion, samples, order, true, name + " put in the host's order"); + Submit(context, [&](VkCommandBuffer commands) { + AgcDriver::Graphics::RecordSampleOrder(context, commands, pass, std::span(&target, 1u), false, kept); + FullBarrier(context, commands); + read.Dispatch(commands, push.data(), SwapExtent); + HostBarrier(context, commands); + }); + Require(ReadValues(values, count) == before, name + ": putting the samples back in the guest's order did not restore every sample bit for bit"); + std::printf("%s round-trips\n", name.c_str()); + return true; +} + +std::vector SwapSampleCounts(const Context& context) { + std::vector counts; + for (const std::uint32_t samples : {2u, 4u, 8u}) { + if ((context.limits.framebufferColorSampleCounts & samples) != 0u && (context.limits.framebufferDepthSampleCounts & samples) != 0u) counts.push_back(samples); + } + return counts; +} + +std::uint32_t SwapPasses(const Context& context) { + std::uint32_t swapped = 0; + for (const auto samples : SwapSampleCounts(context)) { + for (const auto& [format, bytes] : ColorFormats) swapped += ColorSwaps(context, format, bytes, samples) ? 1u : 0u; + swapped += DepthSwaps(context, VK_FORMAT_D32_SFLOAT, samples, SampleOrderMethod::Depth) ? 1u : 0u; + swapped += DepthSwaps(context, VK_FORMAT_D16_UNORM, samples, SampleOrderMethod::Depth) ? 1u : 0u; + for (const auto format : {VK_FORMAT_D32_SFLOAT_S8_UINT, VK_FORMAT_D16_UNORM_S8_UINT}) { + swapped += DepthSwaps(context, format, samples, SampleOrderMethod::Depth) ? 1u : 0u; + swapped += DepthSwaps(context, format, samples, SampleOrderMethod::DepthStencilExport) ? 1u : 0u; + swapped += DepthSwaps(context, format, samples, SampleOrderMethod::DepthStencilBits) ? 1u : 0u; + } + } + return swapped; +} + +using Vertices = std::vector>; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array NullExportCode{0xf8001890, 0x00000000, 0xbf810000}; + +constexpr std::uint32_t RemapWidth = 64; +constexpr std::uint32_t RemapHeight = 8; +alignas(256) std::array Invocations{}; +alignas(256) std::array, 4> StoreCodes{}; +alignas(256) std::array, 8> MaskCodes{}; +alignas(256) constexpr std::array SampleColorCode{0xd5480007, 0x02111106, 0x7e0e0d07, 0xf800180f, 0x07070100, 0xbf810000, 0xbf810000}; +alignas(256) std::array DepthMemory{}; + +std::span StoreCode(std::uint32_t samples) { + const auto shift = static_cast(std::countr_zero(samples)); + auto& code = StoreCodes[shift]; + code = {0x7e100f04, 0x7e120f05, 0x34121286, 0x4a101308, 0x34101080u | shift, 0xd548000a, 0x02111106, 0x4a101508, 0x34101082, 0x7e160281, 0xe0705000, 0x80000b08, 0xbf8c3f70, 0xf8001890, 0x00000000, 0xbf810000, 0xbf810000}; + return code; +} + +std::span MaskCode(std::size_t slot, std::uint32_t mask) { + auto& code = MaskCodes[slot]; + code = {0x7e1002ffu, mask, 0xf8001884u, 0x00080000u, 0xbf810000u}; + return code; +} + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, stride == 0u ? 0x31016facu : 0x01016facu}; +} + +struct Pass { + const Vertices* vertices = nullptr; + const void* vertexMemory = nullptr; + std::span code; + bool perSample = false; + std::uint32_t shaderControl = 0u; + std::uint32_t colorFormat = 0u; + std::uint32_t samples = 4u; + VkExtent2D extent{RemapWidth, RemapHeight}; + std::optional depth; + bool depthWrite = false; + VkCompareOp depthCompare = VK_COMPARE_OP_ALWAYS; + std::optional color; + std::optional> locations; + std::optional order; + std::array quadSampleMask{0xffffu, 0xffffu, 0xffffu, 0xffffu}; + bool storesInvocations = false; + std::function beforeReplay; + bool buildsNoRecipe = false; +}; + +void Draw(AgcDriver::VulkanDevice& device, const Context& context, const Pass& pass) { + const auto target = device.Target(); + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(pass.vertexMemory != nullptr ? pass.vertexMemory : pass.vertices->data(), 16u, static_cast(pass.vertices->size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {WaveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, WaveSize, WaveSize, std::nullopt, std::nullopt}; + state.hasColorTarget = pass.color.has_value(); + if (pass.color) { + VkPipelineColorBlendAttachmentState blend{}; + blend.colorWriteMask = 0xf; + state.colors = {*pass.color}; + state.color = *pass.color; + state.blends = {blend}; + state.blend = blend; + } + state.renderExtent = pass.extent; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(pass.extent.height), static_cast(pass.extent.width), -static_cast(pass.extent.height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, pass.extent}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blendConstants = {}; + state.samples = pass.samples; + state.perSampleShading = pass.perSample; + state.quadSampleMask = pass.quadSampleMask; + state.pixelEarlyZ = AgcDriver::Graphics::PixelEarlyZ(pass.shaderControl); + if (pass.locations) { + state.customSampleLocations = true; + state.sampleLocations = *pass.locations; + for (std::uint32_t pixel = 1; pixel < 4u; ++pixel) { + for (std::uint32_t sample = 0; sample < pass.samples; ++sample) state.sampleLocationGrid |= state.sampleLocations[pixel * 8u + sample] != state.sampleLocations[sample]; + } + } + if (pass.depth) { + state.depth = pass.depth; + state.depthTest = true; + state.depthWrite = pass.depthWrite; + state.depthCompare = pass.depthCompare; + } + + const std::uint32_t inputs = pass.perSample ? PixelInputBit(PixelInput::PerspectiveSample) | PixelInputBit(PixelInput::LinearSample) | PixelInputBit(PixelInput::PositionX) | PixelInputBit(PixelInput::PositionY) | PixelInputBit(PixelInput::Ancillary) : 0u; + const AgcDriver::Registers registers{{0x1b3, inputs}, {0x1b4, inputs}, {0x1b6, 0u}, {0x1c5, pass.colorFormat}, {0x203, pass.shaderControl}}; + auto pixel = AgcDriver::Graphics::DecodePixelStageInfo(registers, AgcDriver::Graphics::ExportMappings(state)); + Require(pixel.sampleShading == pass.perSample, "the pixel inputs did not select the requested shading rate"); + pixel.iteratesSamples = state.perSampleShading; + pixel.sampleLocations = AgcDriver::Graphics::PixelSampleLocations(state, pixel); + const auto order = pass.order ? *pass.order : state.sampleLocationGrid ? AgcDriver::Graphics::HostSampleLocationsFor(context, state).order : SampleOrder{}; + pixel.sampleOrder = order; + pixel.quadPixelMask = AgcDriver::Graphics::QuadPixelMask(state); + pixel.quadSampleMask = AgcDriver::Graphics::QuadSampleMasks(state, order); + std::vector pixelUserData(4, 0u); + if (pass.storesInvocations) { + const auto invocations = BufferDescriptor(Invocations.data(), 0u, static_cast(sizeof(Invocations))); + std::copy(invocations.begin(), invocations.end(), pixelUserData.begin()); + } + const std::array pixelMemory{{{reinterpret_cast(pass.code.data()), std::as_bytes(pass.code)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pass.code.data()), pass.code, 0, {}}, + {WaveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + std::vector shaders{{ShaderStage::Vertex, &vertexResult, 0}}; + ShaderRecompiler::RecompileResult geometry; + if (pixelResult.barycentricEmulation.active) { + geometry = ShaderRecompiler::BuildBarycentricGeometryShader(vertexResult, pixelResult, target, device.GeometryLimits()); + shaders.push_back({ShaderStage::Geometry, &geometry, 0}); + } + shaders.push_back({ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)}); + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(pass.vertices->size()), 0, 1, 0, false}; + if (!pass.beforeReplay && !pass.buildsNoRecipe) { + AgcDriver::Graphics::Draw(context, state, draw, shaders); + return; + } + std::shared_ptr recipe; + AgcDriver::Graphics::Draw(context, state, draw, shaders, {}, &recipe); + if (pass.buildsNoRecipe) { + Require(recipe == nullptr, "a draw that reorders its samples built a recipe"); + return; + } + Require(recipe != nullptr, "the recorded draw built no recipe"); + pass.beforeReplay(); + const auto replayed = AgcDriver::Graphics::DrawWithRecipe(context, state, draw, shaders, {}, *recipe); + Require(replayed.recorded, std::string("the draw did not replay its recipe: ") + AgcDriver::Graphics::DrawRecipeMissName(replayed.miss)); +} + +void Settle(AgcDriver::VulkanDevice& device) { + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + device.SubmitRecorded(true); + } + device.WaitIdle(); +} + +Vertices Rectangle(float left, float right, float z) { + return {{left, -1.0f, z, 1.0f}, {right, -1.0f, z, 1.0f}, {right, 1.0f, z, 1.0f}, {left, -1.0f, z, 1.0f}, {right, 1.0f, z, 1.0f}, {left, 1.0f, z, 1.0f}}; +} + +Vertices Screen(float z) { + return {{-1.0f, -1.0f, z, 1.0f}, {3.0f, -1.0f, z, 1.0f}, {-1.0f, 3.0f, z, 1.0f}}; +} + +float StandardX(std::uint32_t samples, std::uint32_t sample) { + static constexpr std::array two{0.75f, 0.25f}; + static constexpr std::array four{0.375f, 0.875f, 0.125f, 0.625f}; + static constexpr std::array eight{0.5625f, 0.4375f, 0.8125f, 0.3125f, 0.1875f, 0.0625f, 0.6875f, 0.9375f}; + return samples == 2u ? two[sample] : samples == 4u ? four[sample] : eight[sample]; +} + +void CheckStored(std::uint32_t samples, const std::string& what, const std::function& expected) { + for (std::uint32_t y = 0; y < RemapHeight; ++y) { + for (std::uint32_t x = 0; x < RemapWidth; ++x) { + const auto mask = expected(x, y); + for (std::uint32_t sample = 0; sample < samples; ++sample) { + const auto stored = Invocations[(y * RemapWidth + x) * samples + sample]; + Require(stored == ((mask >> sample) & 1u), what + ": guest " + At(x, y, sample) + (stored != 0u ? " ran" : " did not run") + ", expected guest mask " + std::to_string(mask)); + } + } + } +} + +std::uint32_t GuestBits(std::uint32_t hostBits, std::uint32_t word, std::uint32_t samples) { + std::uint32_t guest = 0; + for (std::uint32_t host = 0; host < samples; ++host) { + if (((hostBits >> host) & 1u) != 0u) guest |= 1u << Pick(word, host); + } + return guest; +} + +void SampleIndexRemap(AgcDriver::VulkanDevice& device, const Context& context, std::uint32_t samples) { + const auto order = OrderFor(samples); + const auto toNdc = [](float pixel) { return pixel * 2.0f / static_cast(RemapWidth) - 1.0f; }; + const auto rectangle = Rectangle(toNdc(2.5f), toNdc(5.5f), 0.5f); + for (const bool remapped : {false, true}) { + Invocations.fill(0u); + Pass pass; + pass.vertices = &rectangle; + pass.code = StoreCode(samples); + pass.perSample = true; + pass.samples = samples; + pass.order = remapped ? order : SampleOrder{}; + pass.storesInvocations = true; + Draw(device, context, pass); + Settle(device); + CheckStored(samples, std::to_string(samples) + "x SampleID " + (remapped ? "under the test's sample order" : "in the host's order"), [&](std::uint32_t x, std::uint32_t y) { + std::uint32_t host = 0; + for (std::uint32_t sample = 0; sample < samples; ++sample) { + const auto at = static_cast(x) + StandardX(samples, sample); + if (at > 2.5f && at < 5.5f) host |= 1u << sample; + } + return remapped ? GuestBits(host, order[Quad(x, y)], samples) : host; + }); + } + std::printf("%ux SampleID reads the guest's sample index in all four pixels of the 2x2 grid\n", samples); +} + +void CoverageRemap(AgcDriver::VulkanDevice& device, const Context& context, std::uint32_t samples) { + const auto order = OrderFor(samples); + const auto back = Screen(0.25f), middle = Screen(0.5f), probe = Screen(0.375f); + const AgcDriver::Graphics::DepthTarget depth = [&] { + AgcDriver::Graphics::DepthTarget target{reinterpret_cast(DepthMemory.data()) + 256u * 1024u * static_cast(std::countr_zero(samples)), 0u, {RemapWidth, RemapHeight}, VK_FORMAT_D32_SFLOAT, 0.0f, 0}; + target.samples = samples; + return target; + }(); + const std::array masks{1u, samples == 2u ? 2u : samples == 4u ? 6u : 0x96u}; + std::size_t slot = 0; + for (const auto mask : masks) { + Pass reset; + reset.vertices = &back; + reset.code = NullExportCode; + reset.samples = samples; + reset.depth = depth; + reset.depthWrite = true; + Draw(device, context, reset); + Pass masked = reset; + masked.vertices = &middle; + masked.code = MaskCode(slot++ + (samples == 2u ? 0u : samples == 4u ? 2u : 4u), mask); + masked.shaderControl = 0x100u; + masked.order = order; + Draw(device, context, masked); + Settle(device); + Invocations.fill(0u); + Pass check; + check.vertices = &probe; + check.code = StoreCode(samples); + check.perSample = true; + check.shaderControl = 0x10u; + check.samples = samples; + check.depth = depth; + check.depthCompare = VK_COMPARE_OP_LESS; + check.order = order; + check.storesInvocations = true; + Draw(device, context, check); + Settle(device); + CheckStored(samples, std::to_string(samples) + "x samples a pixel shader's exported guest mask " + std::to_string(mask) + " wrote depth into", [&](std::uint32_t, std::uint32_t) { return mask; }); + } + std::printf("%ux exported sample masks reach the host samples at the guest samples' locations\n", samples); +} + +void CoverageOrder(AgcDriver::VulkanDevice& device, const Context& context, std::uint32_t samples) { + const auto order = OrderFor(samples); + const auto toNdc = [](float pixel) { return pixel * 2.0f / static_cast(RemapWidth) - 1.0f; }; + const auto rectangle = Rectangle(toNdc(2.5f), toNdc(5.5f), 0.5f); + const auto back = Screen(0.25f), probe = Screen(0.375f); + const AgcDriver::Graphics::DepthTarget depth = [&] { + AgcDriver::Graphics::DepthTarget target{reinterpret_cast(DepthMemory.data()) + 256u * 1024u * (static_cast(std::countr_zero(samples)) + 4u), 0u, {RemapWidth, RemapHeight}, VK_FORMAT_D32_SFLOAT, 0.0f, 0}; + target.samples = samples; + return target; + }(); + const std::array mask{2u, 2u, 2u, 4u}; + const auto full = (1u << samples) - 1u; + const auto covered = [&](std::uint32_t x, std::uint32_t y) { + std::uint32_t host = 0u; + for (std::uint32_t sample = 0; sample < samples; ++sample) { + const auto at = static_cast(x) + StandardX(samples, sample); + if (at > 2.5f && at < 5.5f) host |= 1u << sample; + } + return GuestBits(host, order[Quad(x, y)], samples) & (mask[Quad(x, y)] & full); + }; + Pass reset; + reset.vertices = &back; + reset.code = NullExportCode; + reset.samples = samples; + reset.depth = depth; + reset.depthWrite = true; + Draw(device, context, reset); + Invocations.fill(0u); + Pass masked; + masked.vertices = &rectangle; + masked.code = StoreCode(samples); + masked.perSample = true; + masked.samples = samples; + masked.depth = depth; + masked.depthWrite = true; + masked.order = order; + masked.quadSampleMask = mask; + masked.storesInvocations = true; + Draw(device, context, masked); + Settle(device); + CheckStored(samples, std::to_string(samples) + "x a quad sample mask reached the pixel shader in the guest's order", covered); + Invocations.fill(0u); + Pass check; + check.vertices = &probe; + check.code = StoreCode(samples); + check.perSample = true; + check.shaderControl = 0x10u; + check.samples = samples; + check.depth = depth; + check.depthCompare = VK_COMPARE_OP_LESS; + check.order = order; + check.storesInvocations = true; + Draw(device, context, check); + Settle(device); + CheckStored(samples, std::to_string(samples) + "x a quad sample mask exported through the sample order landed on the host's samples", covered); + std::printf("%ux a quad sample mask reaches the pixel shader in the guest's order and exports coverage in the host's\n", samples); +} + +std::uint32_t ShaderRemaps(AgcDriver::VulkanDevice& device, const Context& context) { + std::uint32_t checked = 0; + for (const std::uint32_t samples : {2u, 4u, 8u}) { + if ((context.limits.framebufferDepthSampleCounts & samples) == 0u || (context.limits.framebufferNoAttachmentsSampleCounts & samples) == 0u) continue; + if (context.limits.standardSampleLocations) { + SampleIndexRemap(device, context, samples); + ++checked; + } else { + std::printf("skipped the %ux SampleID check: the device has no standard sample locations\n", samples); + } + CoverageRemap(device, context, samples); + CoverageOrder(device, context, samples); + ++checked; + } + return checked; +} + +constexpr std::size_t BlockBytes = 65536; + +class GuestBlock { +public: + explicit GuestBlock(std::size_t bytes) : bytes((bytes + BlockBytes - 1u) & ~(BlockBytes - 1u)) { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, this->bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(BlockBytes, this->bytes)); +#endif + Require(block != nullptr, "sample order test: cannot allocate a guest block"); + std::memset(block, 0, this->bytes); + GuestAllocations::Mutation().Add(block, this->bytes, true, true, true); + } + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + std::uint64_t Address() const { return static_cast(reinterpret_cast(block)); } + +private: + std::size_t bytes; + std::byte* block = nullptr; +}; + +GuestBlock& NewBlock(std::size_t bytes) { + static auto* blocks = new std::deque(); + return blocks->emplace_back(bytes); +} + +constexpr VkExtent2D ReferenceExtent{64u, 32u}; +constexpr std::array VillageLocations{0x04, 0x0c, 0, 0, 0, 0, 0, 0, 0x04, 0x0c, 0, 0, 0, 0, 0, 0, 0x0c, 0x04, 0, 0, 0, 0, 0, 0, 0x0c, 0x04, 0, 0, 0, 0, 0, 0}; +constexpr float ClearDepth = 0.9375f; +constexpr std::size_t SceneSlots = 6u; +const Vertices Slopes{{-1.0f, -1.0f, 0.125f, 1.0f}, {1.0f, -0.8125f, 0.875f, 1.0f}, {-0.5625f, 1.0f, 0.5f, 1.0f}, {1.0f, -1.0f, 0.75f, 1.0f}, {0.6875f, 1.0f, 0.0625f, 1.0f}, {-1.0f, 0.4375f, 0.3125f, 1.0f}}; + +struct Rendered { + Samples color; + Samples depth; +}; + +double SlopeDepth(std::size_t triangle, double x, double y) { + std::array, 3> corner{}; + for (std::size_t vertex = 0; vertex < corner.size(); ++vertex) { + const auto& at = Slopes[triangle * 3u + vertex]; + corner[vertex] = {(at[0] + 1.0) * ReferenceExtent.width / 2.0, (1.0 - at[1]) * ReferenceExtent.height / 2.0, at[2]}; + } + const auto edge = [](const std::array& from, const std::array& to, double px, double py) { return (to[0] - from[0]) * (py - from[1]) - (to[1] - from[1]) * (px - from[0]); }; + const auto area = edge(corner[0], corner[1], corner[2][0], corner[2][1]); + return (edge(corner[1], corner[2], x, y) * corner[0][2] + edge(corner[2], corner[0], x, y) * corner[1][2] + edge(corner[0], corner[1], x, y) * corner[2][2]) / area; +} + +struct DepthLocations { + std::size_t sloped = 0; + std::size_t guest = 0; + std::size_t standard = 0; + std::array slopedByRow{}; + std::array guestByRow{}; +}; + +DepthLocations LocateDepth(const Samples& depth, const std::array& locations = VillageLocations) { + constexpr std::uint32_t samples = 2u; + constexpr std::array, 2> standard{{{0.75, 0.75}, {0.25, 0.25}}}; + DepthLocations located; + for (std::size_t index = 0; index < depth.size(); ++index) { + const auto value = static_cast(std::bit_cast(depth[index][0])); + if (value == ClearDepth) continue; + ++located.sloped; + const auto pixel = static_cast(index / samples); + const auto sample = static_cast(index % samples); + const auto x = pixel % ReferenceExtent.width; + const auto y = pixel / ReferenceExtent.width; + ++located.slopedByRow[y & 1u]; + const auto bits = static_cast(locations[Quad(x, y) * 8u + sample]); + const std::array guest{0.5 + (static_cast(bits & 7u) - static_cast(bits & 8u)) / 16.0, 0.5 + (static_cast((bits >> 4u) & 7u) - static_cast((bits >> 4u) & 8u)) / 16.0}; + const auto holds = [&](const std::array& at) { + for (std::size_t triangle = 0; triangle < 2u; ++triangle) { + if (std::abs(SlopeDepth(triangle, x + at[0], y + at[1]) - value) < 1.0e-5) return true; + } + return false; + }; + if (holds(guest)) { + ++located.guest; + ++located.guestByRow[y & 1u]; + } else if (holds(standard[sample])) { + ++located.standard; + } + } + return located; +} + +DepthLocations PrintDepthLocations(const std::string& what, const DepthLocations& located) { + std::printf("%s: of %zu sloped depth samples, %zu hold the slope's depth at the guest's sample locations (%zu of %zu on even rows, %zu of %zu on odd rows) and %zu at the standard 2x locations\n", what.c_str(), located.sloped, located.guest, located.guestByRow[0], located.slopedByRow[0], located.guestByRow[1], located.slopedByRow[1], located.standard); + return located; +} + +DepthLocations ReportDepthLocations(const std::string& what, const Rendered& rendered, const std::array& locations = VillageLocations) { + return PrintDepthLocations(what, LocateDepth(rendered.depth, locations)); +} + +Samples ReadTarget(const Context& context, VkImageView view, bool color, std::uint32_t samples) { + const auto count = static_cast(ReferenceExtent.width) * ReferenceExtent.height * samples; + AgcDriver::Graphics::Buffer values(context, count * 16u, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + const std::array bindings{{{0u, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, {2u, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER}}}; + Program read(context, color ? std::span(SAMPLE_ORDER_TEST_READCOLOR_SPV) : std::span(SAMPLE_ORDER_TEST_READDEPTH_SPV), bindings, 12u); + read.Image(0u, view); + read.Storage(2u, values.Handle()); + const std::array push{ReferenceExtent.width, ReferenceExtent.height, samples}; + Submit(context, [&](VkCommandBuffer commands) { + FullBarrier(context, commands); + read.Dispatch(commands, push.data(), ReferenceExtent); + HostBarrier(context, commands); + }); + return ReadValues(values, count); +} + +Rendered RenderCheckerboard(AgcDriver::VulkanDevice& device, const Context& context, std::uint32_t samples, std::size_t run, const std::optional>& locations) { + AgcDriver::Graphics::ColorTarget color{}; + color.extent = ReferenceExtent; + color.surfaceExtent = ReferenceExtent; + color.format = VK_FORMAT_R16G16B16A16_SFLOAT; + color.componentMapping = 0xe4u; + color.tileMode = AgcDriver::Graphics::ColorTileMode::RenderTarget; + color.elementBytes = 8u; + color.samples = samples; + color.bytes = AgcDriver::Graphics::ColorTargetLayout(ReferenceExtent.width, ReferenceExtent.height, color.tileMode, color.elementBytes).Bytes(); + auto& block = NewBlock(AgcDriver::Graphics::MultisampledColorBytes(color)); + color.address = block.Address(); + color.surfaceAddress = color.address; + AgcDriver::Graphics::DepthTarget depth{reinterpret_cast(DepthMemory.data()) + 256u * 1024u * run, 0u, ReferenceExtent, VK_FORMAT_D32_SFLOAT, 1.0f, 0}; + depth.samples = samples; + const Vertices clear = Screen(ClearDepth); + const Vertices shade{{-0.9375f, -0.875f, 0.25f, 1.0f}, {0.9375f, -0.625f, 0.6875f, 1.0f}, {0.0625f, 0.9375f, 0.4375f, 1.0f}}; + Pass depthOnly; + depthOnly.vertices = &clear; + depthOnly.code = NullExportCode; + depthOnly.samples = samples; + depthOnly.extent = ReferenceExtent; + depthOnly.depth = depth; + depthOnly.depthWrite = true; + depthOnly.locations = locations; + Draw(device, context, depthOnly); + depthOnly.vertices = &Slopes; + depthOnly.depthCompare = VK_COMPARE_OP_LESS; + Draw(device, context, depthOnly); + Pass colorPass; + colorPass.vertices = &clear; + colorPass.code = SampleColorCode; + colorPass.perSample = true; + colorPass.colorFormat = 9u; + colorPass.samples = samples; + colorPass.extent = ReferenceExtent; + colorPass.depth = depth; + colorPass.depthCompare = VK_COMPARE_OP_ALWAYS; + colorPass.color = color; + colorPass.locations = locations; + Draw(device, context, colorPass); + colorPass.vertices = &shade; + colorPass.depthCompare = VK_COMPARE_OP_LESS_OR_EQUAL; + Draw(device, context, colorPass); + Settle(device); + Rendered rendered; + rendered.color = ReadTarget(context, AgcDriver::Graphics::MultisampleTargetView(context, color), true, samples); + rendered.depth = ReadTarget(context, AgcDriver::Graphics::DepthSurfaceOrderTarget(context, depth).view, false, samples); + return rendered; +} + +std::size_t Differences(const Samples& reference, const Samples& rendered, std::uint32_t samples, const std::string& what, bool report) { + std::size_t differences = 0; + for (std::size_t index = 0; index < reference.size(); ++index) { + if (reference[index] == rendered[index]) continue; + if (report && differences < 4u) { + const auto pixel = static_cast(index / samples); + std::printf("%s: %s is 0x%08x 0x%08x 0x%08x 0x%08x, the reference's is 0x%08x 0x%08x 0x%08x 0x%08x\n", what.c_str(), At(pixel % ReferenceExtent.width, pixel / ReferenceExtent.width, static_cast(index % samples)).c_str(), rendered[index][0], rendered[index][1], rendered[index][2], rendered[index][3], reference[index][0], reference[index][1], reference[index][2], reference[index][3]); + } + ++differences; + } + return differences; +} + +struct ClearTargets { + AgcDriver::VulkanDevice& device; + VkDevice handle; + ~ClearTargets() { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + device.SubmitRecorded(true); + device.WaitIdle(); + AgcDriver::Graphics::ClearMultisampleTargets(handle); + AgcDriver::Graphics::ClearCachedTextures(handle); + } +}; + +void CheckScene(const Rendered& rendered, std::uint32_t samples, const std::string& what) { + std::size_t cleared = 0; + for (std::size_t index = 0; index < rendered.depth.size(); ++index) { + const auto sample = static_cast(index % samples); + const auto pixel = static_cast(index / samples); + const auto at = At(pixel % ReferenceExtent.width, pixel / ReferenceExtent.width, sample); + const auto id = static_cast(sample); + const auto z = std::bit_cast(rendered.color[index][2]); + const auto w = std::bit_cast(rendered.color[index][3]); + Require(z == id && w == id, what + ": " + at + " does not hold its own sample index in color (z " + std::to_string(z) + ", w " + std::to_string(w) + ")"); + const auto depth = std::bit_cast(rendered.depth[index][0]); + Require(depth >= 0.0f && depth <= ClearDepth, what + ": " + at + " holds depth " + std::to_string(depth)); + if (depth == ClearDepth) ++cleared; + } + Require(cleared != 0u && cleared != rendered.depth.size(), what + ": the slopes did not leave depth both at and below the clear value"); +} + +std::uint32_t SceneChecks(AgcDriver::VulkanDevice& device, const Context& context) { + ClearTargets clearTargets{device, context.device}; + std::uint32_t checked = 0; + for (const std::uint32_t samples : {2u, 4u, 8u}) { + if ((context.limits.framebufferColorSampleCounts & samples) == 0u || (context.limits.framebufferDepthSampleCounts & samples) == 0u || !DepthSampleable(context, VK_FORMAT_D32_SFLOAT, samples)) continue; + CheckScene(RenderCheckerboard(device, context, samples, SceneSlots + static_cast(std::countr_zero(samples)), std::nullopt), samples, std::to_string(samples) + "x scene with the standard locations"); + ++checked; + } + return checked; +} + +std::array UniformLocations(const std::array& pattern, std::uint32_t pixel) { + std::array uniform{}; + for (std::uint32_t target = 0; target < 4u; ++target) std::copy_n(pattern.begin() + 8u * pixel, 8u, uniform.begin() + 8u * target); + return uniform; +} + +Rendered QuadComposite(AgcDriver::VulkanDevice& device, const Context& context, std::uint32_t samples, std::size_t run, const std::array& pattern) { + std::array parts; + for (std::uint32_t pixel = 0; pixel < parts.size(); ++pixel) parts[pixel] = RenderCheckerboard(device, context, samples, run + pixel, UniformLocations(pattern, pixel)); + auto composite = parts[0]; + for (std::size_t index = 0; index < composite.color.size(); ++index) { + const auto pixel = static_cast(index / samples); + const auto& part = parts[Quad(pixel % ReferenceExtent.width, pixel / ReferenceExtent.width)]; + composite.color[index] = part.color[index]; + composite.depth[index] = part.depth[index]; + } + return composite; +} + +int GridReference(AgcDriver::VulkanDevice& device) { + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + ClearTargets clearTargets{device, context.device}; + constexpr std::uint32_t samples = 2u; + if ((context.limits.framebufferColorSampleCounts & samples) == 0u || (context.limits.framebufferDepthSampleCounts & samples) == 0u) { + std::printf("skipped, the device has no 2x color and depth targets\n"); + return VulkanTestSkipped; + } + if ((context.sampleLocationSampleCounts & samples) == 0u) { + std::printf("skipped, the device has no custom sample locations at 2 samples\n"); + return VulkanTestSkipped; + } + const auto largest = context.sampleLocationGridSizes[1]; + std::printf("the device's largest sample location grid at 2 samples is %ux%u pixels (vkGetPhysicalDeviceMultisamplePropertiesEXT)\n", largest.width, largest.height); + auto emulated = context; + emulated.sampleLocationGridSampleCounts = 0u; + auto fallback = emulated; + fallback.storageImageMultisample = false; + AgcDriver::Graphics::State probe{}; + probe.samples = samples; + probe.customSampleLocations = true; + probe.sampleLocationGrid = true; + probe.sampleLocations = VillageLocations; + AgcDriver::Graphics::ColorTarget color{}; + color.format = VK_FORMAT_R16G16B16A16_SFLOAT; + color.elementBytes = 8u; + color.samples = samples; + probe.colors = {color}; + probe.depth = AgcDriver::Graphics::DepthTarget{0x100000u, 0u, ReferenceExtent, VK_FORMAT_D32_SFLOAT, 1.0f, 0}; + probe.depth->samples = samples; + const auto refusal = AgcDriver::Graphics::SampleOrderRefusal(emulated, probe); + if (!refusal.empty()) { + std::printf("skipped, the device does not put the checkerboard's samples in the guest's order: %s\n", refusal.c_str()); + return VulkanTestSkipped; + } + Require(AgcDriver::Graphics::ReordersSamples(AgcDriver::Graphics::HostSampleLocationsFor(emulated, probe).order) && !AgcDriver::Graphics::ReordersSamples(AgcDriver::Graphics::HostSampleLocationsFor(fallback, probe).order), "the emulated and fallback contexts did not take the exact and approximate paths"); + const auto reference = QuadComposite(device, context, samples, 3u, VillageLocations); + const auto exact = RenderCheckerboard(device, emulated, samples, 1u, VillageLocations); + const auto approximate = RenderCheckerboard(device, fallback, samples, 2u, VillageLocations); + CheckScene(reference, samples, "the composite of uniform renders"); + CheckScene(exact, samples, "the exact path"); + CheckScene(approximate, samples, "pixel (0,0)'s locations alone"); + const bool grid = (context.sampleLocationGridSampleCounts & samples) != 0u; + std::optional native; + if (grid) { + native = RenderCheckerboard(device, context, samples, 0u, VillageLocations); + CheckScene(*native, samples, "the 2x2 grid"); + } + const auto referenceDepth = ReportDepthLocations("the composite of uniform renders", reference); + ReportDepthLocations("the exact path", exact); + ReportDepthLocations("pixel (0,0)'s locations alone", approximate); + const auto nativeDepth = native ? std::optional(ReportDepthLocations("the device's 2x2 grid", *native)) : std::nullopt; + const bool gridDepthCompared = nativeDepth && nativeDepth->guest == nativeDepth->sloped; + if (referenceDepth.guest != referenceDepth.sloped) { + std::printf("skipped, the device does not keep depth drawn at custom sample locations at those locations, even after a transition that names them: the composite of uniform renders holds the guest's depth in %zu of %zu sloped samples, so no path can be compared with it bit for bit\n", referenceDepth.guest, referenceDepth.sloped); + return VulkanTestSkipped; + } + std::size_t gridDifferences = 0; + if (native) { + gridDifferences = Differences(reference.color, native->color, samples, "color through the device's 2x2 grid", true) + (gridDepthCompared ? Differences(reference.depth, native->depth, samples, "depth through the device's 2x2 grid", true) : 0u); + if (!gridDepthCompared) std::printf("the device's 2x2 grid holds the guest's depth in %zu of %zu sloped samples as the device reads it back, so only its color is compared with the composite\n", nativeDepth->guest, nativeDepth->sloped); + const auto exactGridDifferences = Differences(native->color, exact.color, samples, "color", false) + Differences(native->depth, exact.depth, samples, "depth", false); + std::printf("2x checkerboard at %ux%u: the device's 2x2 grid differs from the composite of four uniform renders in %zu samples and from the exact path in %zu samples\n", ReferenceExtent.width, ReferenceExtent.height, gridDifferences, exactGridDifferences); + } else { + std::printf("the device has no 2x2 sample location grid at 2 samples: the composite of four uniform renders is the reference\n"); + } + const auto colorDifferences = Differences(reference.color, exact.color, samples, "color with the samples put in the guest's order", true); + const auto depthDifferences = Differences(reference.depth, exact.depth, samples, "depth with the samples put in the guest's order", true); + const auto approximateDifferences = Differences(reference.color, approximate.color, samples, "color", false) + Differences(reference.depth, approximate.depth, samples, "depth", false); + std::printf("2x checkerboard at %ux%u: the exact path differs from the reference in %zu color and %zu depth samples; pixel (0,0)'s locations alone differ in %zu samples\n", ReferenceExtent.width, ReferenceExtent.height, colorDifferences, depthDifferences, approximateDifferences); + Require(approximateDifferences != 0u, "pixel (0,0)'s locations alone rendered the checkerboard like the reference: the scene does not tell the paths apart"); + Require(gridDifferences == 0u, "the composite of four uniform renders does not render the checkerboard like the device's 2x2 grid"); + Require(colorDifferences == 0u && depthDifferences == 0u, "the exact path did not render the checkerboard bit for bit like the reference"); + std::puts(grid && gridDepthCompared ? "the exact path renders Village's 2x checkerboard like the device's 2x2 sample location grid, bit for bit" : "the exact path renders Village's 2x checkerboard like four uniform renders composed by pixel parity, bit for bit"); + return 0; +} + +void CheckRecipeDepth(AgcDriver::VulkanDevice& device, const Context& context, const std::array& locations, std::size_t run, const std::string& what, bool reordered) { + constexpr std::uint32_t samples = 2u; + AgcDriver::Graphics::DepthTarget depth{reinterpret_cast(DepthMemory.data()) + 256u * 1024u * run, 0u, ReferenceExtent, VK_FORMAT_D32_SFLOAT, 1.0f, 0}; + depth.samples = samples; + const Vertices clear = Screen(ClearDepth); + Pass clearPass; + clearPass.vertices = &clear; + clearPass.code = NullExportCode; + clearPass.samples = samples; + clearPass.extent = ReferenceExtent; + clearPass.depth = depth; + clearPass.depthWrite = true; + clearPass.locations = locations; + auto slopes = clearPass; + slopes.vertices = &Slopes; + auto& vertexBlock = NewBlock(Slopes.size() * sizeof(Slopes[0])); + std::memcpy(reinterpret_cast(static_cast(vertexBlock.Address())), Slopes.data(), Slopes.size() * sizeof(Slopes[0])); + slopes.vertexMemory = reinterpret_cast(static_cast(vertexBlock.Address())); + if (reordered) { + slopes.buildsNoRecipe = true; + Draw(device, context, clearPass); + Draw(device, context, slopes); + Settle(device); + std::printf("%s: builds no recipe, since a draw that reorders its samples is built in full each time, with its sample swaps\n", what.c_str()); + return; + } + const auto read = [&] { return ReadTarget(context, AgcDriver::Graphics::DepthSurfaceOrderTarget(context, depth).view, false, samples); }; + Samples recorded; + slopes.beforeReplay = [&] { + Settle(device); + recorded = read(); + Draw(device, context, clearPass); + Settle(device); + const auto cleared = read(); + Require(std::all_of(cleared.begin(), cleared.end(), [](const std::array& value) { return std::bit_cast(value[0]) == ClearDepth; }), what + ": the clear between the recorded draw and its replay left depth other than the clear value"); + }; + Draw(device, context, clearPass); + Draw(device, context, slopes); + Settle(device); + const auto replayed = read(); + const auto first = PrintDepthLocations(what + ", the recorded draw", LocateDepth(recorded, locations)); + PrintDepthLocations(what + ", the recipe's replay as the first draw of a pass", LocateDepth(replayed, locations)); + Require(first.sloped != 0u, what + ": the recorded draw left no sloped depth"); + const bool readAtGuestLocations = first.guest == first.sloped; + if (!readAtGuestLocations) std::printf("%s: the device reads depth drawn at custom sample locations back at other locations, even after a transition that names them: the replay is compared with the recorded draw as the device reads both back\n", what.c_str()); + const auto differences = Differences(recorded, replayed, samples, what + ", depth replayed from the recipe", true); + std::printf("%s, %ux%u: the recipe's replay differs from the recorded draw in %zu of %zu samples\n", what.c_str(), ReferenceExtent.width, ReferenceExtent.height, differences, recorded.size()); + Require(differences == 0u, what + (readAtGuestLocations ? ": the recipe's replay did not leave the depth the recorded draw left at the guest's sample locations" : ": the recipe's replay did not leave the depth the recorded draw left, as the device reads both back")); + std::printf("%s: a depth-only 2x draw replayed from its recipe as the first draw of a pass leaves the depth the recorded draw left%s, bit for bit\n", what.c_str(), readAtGuestLocations ? "" : " as the device reads both back"); +} + +int RecipeDepthLocations(AgcDriver::VulkanDevice& device) { + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + ClearTargets clearTargets{device, context.device}; + constexpr std::uint32_t samples = 2u; + if ((context.limits.framebufferDepthSampleCounts & samples) == 0u || !DepthSampleable(context, VK_FORMAT_D32_SFLOAT, samples)) { + std::printf("skipped, the device has no sampleable 2x depth targets\n"); + return VulkanTestSkipped; + } + if ((context.sampleLocationSampleCounts & samples) == 0u || !AgcDriver::Graphics::SampleLocationsCompatibleDepth(context, VK_FORMAT_D32_SFLOAT, samples)) { + std::printf("skipped, the device has no custom sample locations at 2 samples\n"); + return VulkanTestSkipped; + } + if (!AgcDriver::Graphics::DrawRecipes()) { + std::printf("skipped, draw recipes are off (APS5_NO_DRAW_RECIPE)\n"); + return VulkanTestSkipped; + } + CheckRecipeDepth(device, context, UniformLocations(VillageLocations, 0u), 0u, "pixel (0,0)'s locations in every pixel", false); + auto emulated = context; + emulated.sampleLocationGridSampleCounts = 0u; + AgcDriver::Graphics::State probe{}; + probe.samples = samples; + probe.customSampleLocations = true; + probe.sampleLocationGrid = true; + probe.sampleLocations = VillageLocations; + probe.depth = AgcDriver::Graphics::DepthTarget{0x100000u, 0u, ReferenceExtent, VK_FORMAT_D32_SFLOAT, 1.0f, 0}; + probe.depth->samples = samples; + const auto refusal = AgcDriver::Graphics::SampleOrderRefusal(emulated, probe); + if (!refusal.empty()) { + std::printf("the checkerboard through the exact path is not checked: the device does not put its samples in the guest's order: %s\n", refusal.c_str()); + return 0; + } + Require(AgcDriver::Graphics::ReordersSamples(AgcDriver::Graphics::HostSampleLocationsFor(emulated, probe).order), "the context without a 2x2 grid did not take the exact path"); + CheckRecipeDepth(device, emulated, UniformLocations(VillageLocations, 0u), 1u, "pixel (0,0)'s locations in every pixel, without a 2x2 grid", false); + CheckRecipeDepth(device, emulated, VillageLocations, 2u, "the 2x checkerboard through the exact path", true); + return 0; +} + +int Bench(AgcDriver::VulkanDevice& device, VkExtent2D extent, std::uint32_t samples, std::uint32_t frames) { + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + if ((context.limits.framebufferColorSampleCounts & samples) == 0u || (context.limits.framebufferDepthSampleCounts & samples) == 0u) { + std::printf("skipped, the device has no %ux color and depth targets\n", samples); + return VulkanTestSkipped; + } + const auto alias = AgcDriver::Graphics::SampleOrderStorageFormat(context, VK_FORMAT_R16G16B16A16_SFLOAT, 8u, samples); + if (alias == VK_FORMAT_UNDEFINED || !DepthSampleable(context, VK_FORMAT_D32_SFLOAT, samples) || !context.limits.timestampComputeAndGraphics) { + std::printf("skipped, the device has no %ux storage alias for RGBA16F, sampleable D32F or timestamps\n", samples); + return VulkanTestSkipped; + } + const bool located = (context.sampleLocationSampleCounts & samples) != 0u && AgcDriver::Graphics::SampleLocationsCompatibleDepth(context, VK_FORMAT_D32_SFLOAT, samples); + Image color(context, VK_FORMAT_R16G16B16A16_SFLOAT, samples, extent, ColorUsage, VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | VK_IMAGE_CREATE_EXTENDED_USAGE_BIT); + Image depth(context, VK_FORMAT_D32_SFLOAT, samples, extent, DepthUsage, located ? VK_IMAGE_CREATE_SAMPLE_LOCATIONS_COMPATIBLE_DEPTH_BIT_EXT : 0u); + const auto storage = color.View(alias, VK_IMAGE_ASPECT_COLOR_BIT, VK_IMAGE_USAGE_STORAGE_BIT); + const auto attachment = depth.View(VK_FORMAT_D32_SFLOAT, VK_IMAGE_ASPECT_DEPTH_BIT, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT); + const std::array targets{{ + {color.Handle(), storage, alias, extent, samples, SampleOrderMethod::Storage, nullptr}, + {depth.Handle(), attachment, VK_FORMAT_D32_SFLOAT, extent, samples, SampleOrderMethod::Depth, nullptr}, + }}; + VkQueryPoolCreateInfo queryInfo{VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO}; + queryInfo.queryType = VK_QUERY_TYPE_TIMESTAMP; + queryInfo.queryCount = 5u; + VkQueryPool queries = VK_NULL_HANDLE; + Check(context.Function("vkCreateQueryPool")(context.device, &queryInfo, nullptr, &queries), "vkCreateQueryPool sample order bench"); + Submit(context, [&](VkCommandBuffer commands) { + color.ToGeneral(commands, VK_IMAGE_ASPECT_COLOR_BIT); + depth.ToGeneral(commands, VK_IMAGE_ASPECT_DEPTH_BIT); + }); + const auto measure = [&](const SampleOrderPass& pass) { + std::vector> phases; + std::vector> kept; + for (std::uint32_t frame = 0; frame <= frames; ++frame) { + kept.clear(); + Submit(context, [&](VkCommandBuffer commands) { + context.Function("vkCmdResetQueryPool")(commands, queries, 0u, 5u); + const auto stamp = [&](std::uint32_t query) { + context.Function("vkCmdWriteTimestamp")(commands, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, queries, query); + }; + stamp(0u); + AgcDriver::Graphics::RecordSampleOrder(context, commands, pass, std::span(targets).subspan(1u), true, kept); + stamp(1u); + AgcDriver::Graphics::RecordSampleOrder(context, commands, pass, std::span(targets).subspan(1u), false, kept); + stamp(2u); + AgcDriver::Graphics::RecordSampleOrder(context, commands, pass, targets, true, kept); + stamp(3u); + AgcDriver::Graphics::RecordSampleOrder(context, commands, pass, targets, false, kept); + stamp(4u); + }); + std::array ticks{}; + Check(context.Function("vkGetQueryPoolResults")(context.device, queries, 0u, 5u, sizeof(ticks), ticks.data(), sizeof(std::uint64_t), VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT), "vkGetQueryPoolResults sample order bench"); + if (frame == 0u) continue; + std::array phase{}; + for (std::size_t index = 0; index < phase.size(); ++index) phase[index] = static_cast(ticks[index + 1u] - ticks[index]) * context.limits.timestampPeriod / 1.0e6; + phases.push_back(phase); + } + std::array median{}; + for (std::size_t index = 0; index < median.size(); ++index) { + std::vector values; + for (const auto& phase : phases) values.push_back(phase[index]); + std::nth_element(values.begin(), values.begin() + static_cast(values.size() / 2u), values.end()); + median[index] = values[values.size() / 2u]; + } + return median; + }; + const auto report = [&](const std::array& median, const char* how) { + std::printf("sample order swaps at %ux%u, %ux, %s, median of %u frames (GPU ms): depth-only pass in %.3f, out %.3f; color+depth pass in %.3f, out %.3f; per frame with one pass of each %.3f\n", extent.width, extent.height, samples, how, frames, median[0], median[1], median[2], median[3], median[0] + median[1] + median[2] + median[3]); + }; + report(measure(SampleOrderPass{OrderFor(samples), extent, {}, false}), "without sample locations"); + if (located) { + AgcDriver::Graphics::State standard{}; + standard.samples = samples; + report(measure(SampleOrderPass{OrderFor(samples), extent, AgcDriver::Graphics::SampleLocations(standard), true}), "naming the standard sample locations at the depth copy's transitions"); + } else { + std::printf("the device draws no %ux depth at custom sample locations: no swap names them\n", samples); + } + context.Function("vkDestroyQueryPool")(context.device, queries, nullptr); + return 0; +} + +} + +int main(int argc, char** argv) { + try { + const std::string_view mode = argc > 1 ? std::string_view(argv[1]) : std::string_view(); + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (mode == "--grid-reference") return GridReference(*device); + if (mode == "--recipe-depth-locations") return RecipeDepthLocations(*device); + if (mode == "--bench") { + VkExtent2D extent{1920u, 2160u}; + if (argc > 2) Require(std::sscanf(argv[2], "%ux%u", &extent.width, &extent.height) == 2, "--bench takes WIDTHxHEIGHT"); + const auto samples = argc > 3 ? static_cast(std::strtoul(argv[3], nullptr, 10)) : 2u; + const auto frames = argc > 4 ? static_cast(std::strtoul(argv[4], nullptr, 10)) : 50u; + return Bench(*device, extent, samples, frames); + } + Require(mode.empty(), "unknown mode " + std::string(mode)); + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(*device); + if (!context.sampleRateShading) { + std::printf("skipped, the device lacks sampleRateShading\n"); + return VulkanTestSkipped; + } + const auto swapped = SwapPasses(context); + const auto remapped = ShaderRemaps(*device, context); + const auto scenes = SceneChecks(*device, context); + if (swapped == 0u && remapped == 0u && scenes == 0u) { + std::printf("skipped, the device has no multisampled targets the sample order tests can use\n"); + return VulkanTestSkipped; + } + std::printf("sample order tests passed: %u swap round trips, %u shader remap checks, %u scene checks\n", swapped, remapped, scenes); + 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..46e3184ea6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp @@ -9,6 +9,8 @@ #include "prx/libSceAgcDriver/Graphics/include/TextureCache.hpp" #include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp" #include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" #include "VulkanTestDevice.hpp" #include #ifdef _WIN32 @@ -16,8 +18,12 @@ #define NOMINMAX #endif #include +#else +#include #endif +#include #include +#include #include #include #include @@ -26,8 +32,11 @@ #include #include #include +#include +#include #include #include +#include #include #include #include @@ -48,6 +57,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 +141,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 +198,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 +218,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 { @@ -194,6 +279,53 @@ private: void* memory = nullptr; }; +class WatchedSurface { +public: + explicit WatchedSurface(const Context& context) : context(context) { +#ifdef _WIN32 + memory = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(SurfaceBytes, 65536)); + if (memory != nullptr) GuestArena::GuestArenaCommit_nid_postfix(memory, SurfaceBytes, PAGE_READWRITE, SurfaceBytes); +#else + constexpr std::uintptr_t alignment = 65536; + void* mapped = mmap(nullptr, SurfaceBytes + alignment, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapped != MAP_FAILED) { + const auto begin = reinterpret_cast(mapped); + const auto aligned = (begin + alignment - 1) & ~(alignment - 1); + if (aligned != begin) munmap(mapped, aligned - begin); + if (aligned + SurfaceBytes != begin + SurfaceBytes + alignment) munmap(reinterpret_cast(aligned + SurfaceBytes), begin + alignment - aligned); + memory = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(memory, SurfaceBytes); + } +#endif + Require(memory != nullptr, "cannot allocate the watched surface"); + Require(AgcDriver::GuestMemory::Watched(Address(), SurfaceBytes), "the watched surface is not write-watched"); + std::memset(memory, 0x5a, SurfaceBytes); + GuestAllocations::Mutation().Add(memory, SurfaceBytes, true, true, true); + AgcDriver::GuestMemory::CollectWritesUncached(Address(), SurfaceBytes); + } + + ~WatchedSurface() { + ClearCachedTextures(context.device); + GuestAllocations::Mutation().Remove(memory); +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(memory, SurfaceBytes); + GuestArena::GuestArenaRelease_nid_postfix(memory, SurfaceBytes); +#else + munmap(memory, SurfaceBytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(memory, SurfaceBytes); +#endif + } + + WatchedSurface(const WatchedSurface&) = delete; + WatchedSurface& operator=(const WatchedSurface&) = delete; + + std::uint64_t Address() const { return reinterpret_cast(memory); } + +private: + const Context& context; + std::uint8_t* memory = nullptr; +}; + std::array Descriptor(std::uint64_t address, std::uint32_t side = Side) { return { static_cast(address >> 8u), @@ -399,6 +531,103 @@ void viewTests(const Context& context) { Require(heldView.expired() && heldStorage.expired(), "the evicted image outlived its last caller without a sampled lookup"); } +ShaderRecompiler::RecompileResult SampledProgram(std::uint64_t address) { + ShaderRecompiler::DescriptorBinding binding{}; + binding.kind = ShaderRecompiler::DescriptorKind::SampledImage; + binding.role = ShaderRecompiler::DescriptorRole::GuestImages; + binding.descriptorSet = 0; + binding.binding = ShaderRecompiler::RuntimeAbi::FirstImageBinding + PlaceholderDimensionsPerClass + 2u; + binding.count = 1; + binding.imageShape = ShaderRecompiler::DescriptorImageShape::Image2D; + binding.imageWritten.assign(1, false); + binding.imageSamplers.assign(1, 0u); + const auto words = Descriptor(address); + binding.guestDescriptor.assign(words.begin(), words.end()); + ShaderRecompiler::RecompileResult program; + program.bindings.push_back(std::move(binding)); + return program; +} + +void replacedEntryTests(Context context) { + PlaceholderImages placeholders(context); + context.placeholderImages = &placeholders; + TextureCache textures(context); + context.textureCache = &textures; + WatchedSurface surface(context); + ClearCachedTextures(context.device); + const auto program = SampledProgram(surface.Address()); + const CompiledShader shader{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + auto first = std::make_shared(context, shader); + Require(first->Reusable(), "the sampled snapshot's descriptor set is not eligible for resource caching"); + Require(first->SampledTextures().size() == 1, "the sampled descriptor set binds no texture"); + const auto bound = first->SampledTextures().front(); + Require(!bound->ViewsStorageImage(), "the replaced-entry fixture is not a snapshot"); + Require(Sampled(context, surface.Address()) == bound, "the descriptor set does not bind the cache's texture"); + ShaderResources::ProofReport report{}; + Require(first->Revalidate(shader), "a template over an unchanged, still cached texture did not prove current"); + Require(first->Revalidate(shader, &report), "a template over an unchanged, still cached texture did not prove current twice"); + Require(report.path == ShaderResources::ProofPath::Fast, "the fixture's template did not take the fast proof"); + Require(first->SampledTextures().front() == bound, "the proof changed the bound texture"); + ClearCachedTextures(context.device); + Require(TextureCacheUsage().sampledEntries == 0, "clearing the sampled cache left entries"); + Require(!first->Revalidate(shader, &report), "a template binding a texture whose cache entry left still proved current"); + const auto replacement = Sampled(context, surface.Address()); + Require(replacement != bound, "the lookup after the clear handed back the old texture"); + Require(!first->Revalidate(shader), "a template binding the replaced texture proved current against the new entry"); + auto rebuilt = std::make_shared(context, shader); + Require(rebuilt->SampledTextures().front() == replacement, "the rebuilt descriptor set does not bind the cache's new texture"); + Require(rebuilt->Revalidate(shader), "the rebuilt template did not prove current over its cached texture"); + first.reset(); + rebuilt.reset(); + ClearCachedTextures(context.device); + std::puts("template over a replaced sampled cache entry passed"); +} + +bool lookupChangedTests(Context context) { + PlaceholderImages placeholders(context); + context.placeholderImages = &placeholders; + TextureCache textures(context); + context.textureCache = &textures; + if (context.hostImportAlignment == 0) { + Require(std::getenv("ANYPS5_REQUIRE_VULKAN") == nullptr, "the lookup-changed test requires host imports"); + std::puts("skipped, host imports are unavailable"); + return false; + } + WatchedSurface surface(context); + ClearCachedTextures(context.device); + const auto program = SampledProgram(surface.Address()); + const CompiledShader shader{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + auto first = std::make_shared(context, shader); + Require(first->Reusable() && first->SampledTextures().size() == 1, "the lookup-changed template is not reusable"); + const auto bound = first->SampledTextures().front(); + Require(!bound->ViewsStorageImage(), "the lookup-changed fixture is not a snapshot before the import"); + ShaderResources::ProofReport report{}; + Require(first->Revalidate(shader), "the lookup-changed template did not prove current"); + Require(first->Revalidate(shader, &report) && report.path == ShaderResources::ProofPath::Fast, "the lookup-changed template did not take the fast proof"); + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + Require(first->Revalidate(shader, &report), "the audit refused an unchanged template"); + Require(report.path == ShaderResources::ProofPath::Full, "a proof past the audit interval did not take the full walk"); + Require(first->SampledTextures().front() == bound, "the audit changed the bound texture"); + Require(first->Revalidate(shader, &report) && report.path == ShaderResources::ProofPath::Fast, "the proof right after an audit did not take the fast proof"); + if (HostImportFor(context, surface.Address(), SurfaceBytes) == nullptr) { + Require(std::getenv("ANYPS5_REQUIRE_VULKAN") == nullptr, "the host import of the watched surface was refused"); + std::puts("skipped, host imports are unavailable"); + return false; + } + Require(!bound->Released(), "the snapshot left the cache without a lookup"); + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + Require(!first->Revalidate(shader, &report), "a template binding a snapshot of a surface its lookup now views through a storage image still proved current past the audit interval"); + const auto view = Sampled(context, surface.Address()); + Require(view != bound && view->ViewsStorageImage(), "the lookup of the imported surface does not view its storage image"); + auto rebuilt = std::make_shared(context, shader); + Require(rebuilt->SampledTextures().front() == view, "the rebuilt template does not bind the storage view"); + first.reset(); + rebuilt.reset(); + ClearCachedTextures(context.device); + std::puts("template over a changed sampled lookup passed"); + return true; +} + void reportedBudgetTests(const Context& context) { if (context.memoryProperties2 == nullptr) { std::puts("VK_EXT_memory_budget unavailable: the sampled texture cache keeps a quarter of the device-local heap"); @@ -563,10 +792,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 +807,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); @@ -666,7 +960,7 @@ std::uint64_t ReadBudget(const Context& context) { return SampledTextureCacheBudget(context); } -constexpr std::uint64_t PressedUsage = ReportedBudget - 11ull * (1ull << 30u); +constexpr std::uint64_t PressedUsage = ReportedBudget - 9ull * (1ull << 30u); void VKAPI_PTR ReportPressure(VkPhysicalDevice, VkPhysicalDeviceMemoryProperties2* properties) { auto* reported = static_cast(properties->pNext); @@ -707,8 +1001,8 @@ void storageMemoryTests(Context context) { Require(ReadStorageBudget(context) == quarter, "52 GiB free of a 64 GiB budget does not leave the storage image cache its quarter"); context.memoryProperties2 = &ReportPressure; const auto pressed = ReadStorageBudget(context); - const auto expected = GiB + SampledTextureMemory() + TextureCacheUsage().storageBytes; - Require(pressed == expected, "53 GiB in use of a 64 GiB budget leaves the storage image cache " + std::to_string(pressed >> 20u) + " MiB, not the " + std::to_string(expected >> 20u) + " MiB left after the use outside the texture caches, the sampled textures' 2 GiB floor and 8 GiB of headroom"); + const auto expected = GiB + TextureCacheUsage().storageBytes; + Require(pressed == expected, "55 GiB in use of a 64 GiB budget leaves the storage image cache " + std::to_string(pressed >> 20u) + " MiB, not the " + std::to_string(expected >> 20u) + " MiB left after the use outside the texture caches, the sampled textures and 8 GiB of headroom"); std::printf("reported heap budget %llu MiB with %llu MiB in use: storage image cache budget %llu MiB\n", static_cast(ReportedBudget >> 20u), static_cast(PressedUsage >> 20u), static_cast(pressed >> 20u)); } @@ -718,21 +1012,32 @@ 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 if (!memory) _putenv_s("APS5_TEXTURE_CACHE_MIB", "1"); + if (std::getenv("APS5_TEMPLATE_AUDIT_MS") == nullptr) _putenv_s("APS5_TEMPLATE_AUDIT_MS", "250"); #else if (!memory) setenv("APS5_TEXTURE_CACHE_MIB", "1", 1); + setenv("APS5_TEMPLATE_AUDIT_MS", "250", 0); #endif std::unique_ptr device; 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); @@ -757,6 +1062,18 @@ int main(int argc, char** argv) { std::puts("resource cache tests passed"); return 0; } + if (mode == "resource-replaced") { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + replacedEntryTests(context); + std::puts("resource cache replaced entry tests passed"); + return 0; + } + if (mode == "resource-lookup-changed") { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + if (!lookupChangedTests(context)) return VulkanTestSkipped; + std::puts("resource cache lookup-changed tests passed"); + return 0; + } if (memory) { std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); memoryTests(context); @@ -774,7 +1091,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/SgprPatternFillKeys.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SgprPatternFillKeys.cpp new file mode 100644 index 0000000000..c6dd1e4ca1 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SgprPatternFillKeys.cpp @@ -0,0 +1,234 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Memory/DwordPatternFill.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::DriverDetail::MatchSgprPatternFill; +using AgcDriver::Graphics::DccKeys; +using AgcDriver::Graphics::Require; + +alignas(256) constexpr std::array Code{ + 0xbfa00001u, 0xd7460004u, 0x04010c09u, 0x7e000204u, 0x7e020205u, 0x7e040206u, 0x7e060207u, 0x7da80808u, + 0xbf88000du, 0x360a0883u, 0xbe84047eu, 0xbe86047eu, 0x7e100505u, 0x7da40a08u, 0xbefc0308u, 0x7e0c8700u, + 0x8a867e06u, 0xbefe0406u, 0xbf85fff9u, 0xbefe0404u, 0xe0102000u, 0x80000604u, 0xbf810000u}; +constexpr std::uint32_t Dwords = 1024; +constexpr std::size_t KeyBytes = Dwords * 4; +constexpr std::uint64_t SurfaceBytes = KeyBytes * 256; +alignas(256) std::array Keys{}; + +using Pattern = std::array; + +constexpr Pattern Uniform(std::uint8_t key) { + const std::uint32_t word = key * 0x01010101u; + return {word, word, word, word}; +} + +std::uint64_t KeysAddress(std::size_t offset = 0) { + return static_cast(reinterpret_cast(Keys.data())) + offset; +} + +std::vector UserData(std::uint32_t records, std::uint32_t count, const Pattern& pattern, std::uint32_t format = 0x00014004u) { + const auto address = KeysAddress(); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (4u << 16u), records, format, pattern[0], pattern[1], pattern[2], pattern[3], count}; +} + +void MatchTests() { + const ShaderRecompiler::ShaderComputeStageInfo compute{{64, 1, 1}, 0, {true, false, false}, false, 1}; + const std::array packet{0xc0031500u, 16, 1, 1, 0x41u}; + const Pattern pattern{0x11111111u, 0x22222222u, 0x33333333u, 0x44444444u}; + const auto data = UserData(Dwords, Dwords, pattern); + const auto fill = MatchSgprPatternFill(packet, Code, data, compute); + Require(fill && fill->base == KeysAddress() && fill->bytes == KeyBytes && fill->pattern == pattern, "the SGPR pattern-fill kernel was not recognized"); + for (std::size_t word = 0; word < Code.size(); ++word) { + auto altered = Code; + altered[word] ^= 1u; + Require(!MatchSgprPatternFill(packet, altered, data, compute), "a different shader matched the SGPR pattern-fill kernel"); + } + Require(!MatchSgprPatternFill(packet, std::span(Code).first(22), data, compute), "a truncated shader matched"); + const auto bounded = MatchSgprPatternFill(packet, Code, UserData(Dwords, 100, pattern), compute); + Require(bounded && bounded->bytes == 400, "the fill exceeded the kernel's count"); + const auto clamped = MatchSgprPatternFill(packet, Code, UserData(64, Dwords, pattern), compute); + Require(clamped && clamped->bytes == 256, "the fill exceeded the descriptor's records"); + auto fewer = packet; + fewer[1] = 2; + const auto launched = MatchSgprPatternFill(fewer, Code, data, compute); + Require(launched && launched->bytes == 512, "the fill exceeded the launched threads"); + Require(!MatchSgprPatternFill(packet, Code, UserData(Dwords, 0, pattern), compute), "an empty fill matched"); + for (const std::uint32_t format : {0x00015004u, 0x00016004u, 0x10014004u}) { + Require(MatchSgprPatternFill(packet, Code, UserData(Dwords, Dwords, pattern, format), compute).has_value(), "a 32-bit output format with an index bound was refused"); + } + for (const std::uint32_t format : {0x00005004u, 0x0000d004u, 0x0004b004u, 0x00814004u, 0x20014004u, 0x30014004u, 0x40014004u}) { + Require(!MatchSgprPatternFill(packet, Code, UserData(Dwords, Dwords, pattern, format), compute), "a descriptor whose elements are not one bounded 32-bit value matched"); + } + auto wide = data; + wide[1] = (wide[1] & 0xffffu) | (8u << 16u); + Require(!MatchSgprPatternFill(packet, Code, wide, compute), "a stride other than one dword matched"); + auto swizzled = data; + swizzled[1] |= 1u << 31u; + Require(!MatchSgprPatternFill(packet, Code, swizzled, compute), "a swizzled descriptor matched"); + auto extra = data; + extra.push_back(0); + Require(!MatchSgprPatternFill(packet, Code, extra, compute), "a different user SGPR count matched (the group id moves)"); + auto partial = packet; + partial[4] |= 0x20u; + Require(!MatchSgprPatternFill(partial, Code, data, compute), "a partial dispatch matched"); + auto rows = packet; + rows[2] = 2; + Require(!MatchSgprPatternFill(rows, Code, data, compute), "a dispatch with more than one group row matched"); + auto noGroup = compute; + noGroup.groupIdEnable[0] = false; + Require(!MatchSgprPatternFill(packet, Code, data, noGroup), "a dispatch without the group id matched"); +} + +alignas(8) std::uint32_t Finished = 0; +std::uint32_t Submitted = 0; +constexpr std::uint32_t MaxResubmits = 16; + +void Submit(const std::vector& data, std::uint32_t groups) { + 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(data.size()) << 1u) | (1u << 7u)}; + registers(0x213, resource); + registers(0x240, data); + words.insert(words.end(), {0xc0031500u, groups, 1, 1, 0x41u}); + const auto finished = reinterpret_cast(&Finished); + words.insert(words.end(), {0xc0033700u, 0x00100200u, static_cast(finished), static_cast(finished >> 32u), ++Submitted}); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); +} + +struct KeyRead { + DccKeys keys; + bool waited; + std::uint32_t resubmits; +}; + +std::uint64_t lastNote = 0; + +void Settle() { + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::GuestMemory::FlushGpuWrites(KeysAddress(), KeyBytes); +} + +KeyRead SubmitAndRead(const std::vector& data, std::uint32_t groups, std::uint64_t surfaceBytes = SurfaceBytes, bool retireFirst = false) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + Submit(data, groups); + if (retireFirst) Settle(); + std::uint32_t resubmits = 0; + for (;;) { + bool retired = false; + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + auto* recorder = AgcDriver::Graphics::Recorder::Active(); + if (const auto noted = recorder != nullptr ? recorder->NewestWriteNote(KeysAddress(), KeyBytes) : 0; noted > lastNote) { + lastNote = noted; + const auto keys = AgcDriver::Graphics::CurrentDccKeys(KeysAddress(), surfaceBytes); + return {keys, !recorder->PendingWriteOverlaps(KeysAddress(), KeyBytes), resubmits}; + } + retired = std::atomic_ref(Finished).load() == Submitted; + } + if (retired) { + Require(resubmits < MaxResubmits, "the dispatch completed " + std::to_string(resubmits + 1) + " times without a pending write over the keys"); + ++resubmits; + Submit(data, groups); + } + Require(std::chrono::steady_clock::now() < deadline, "the submitted dispatch never became a pending write over the keys"); + std::this_thread::yield(); + } +} + +void RequireStored(std::uint32_t count, const Pattern& pattern, std::uint32_t before, const std::string& what) { + Settle(); + for (std::uint32_t i = 0; i < Dwords; ++i) { + Require(Keys[i] == (i < count ? pattern[i % 4] : before), what + ": dword " + std::to_string(i) + " holds " + std::to_string(Keys[i])); + } +} + +std::string Describe(const char* what, const KeyRead& read) { + return std::string(what) + ": the keys read " + AgcDriver::Graphics::DccKeysName(read.keys) + " and the read " + (read.waited ? "waited for the GPU" : "did not wait"); +} + +void ExecuteTests() { + const std::array, 6> codes{{{0x00, DccKeys::Clear0000}, {0x40, DccKeys::Clear0001}, {0x80, DccKeys::Clear1110}, {0xc0, DccKeys::Clear1111}, {0x20, DccKeys::ClearRegister}, {0xff, DccKeys::Uncompressed}}}; + for (const auto& [key, keys] : codes) { + Settle(); + const auto read = SubmitAndRead(UserData(Dwords, Dwords, Uniform(key)), Dwords / 64); + Require(read.keys == keys && !read.waited, Describe("a pending key code fill by the kernel", read)); + RequireStored(Dwords, Uniform(key), 0, "the kernel's key code fill"); + } + + Settle(); + const auto retired = SubmitAndRead(UserData(Dwords, Dwords, Uniform(0x40)), Dwords / 64, SurfaceBytes, true); + Require(retired.resubmits > 0, "a fill that retired before the read was not submitted again"); + Require(retired.keys == DccKeys::Clear0001 && !retired.waited, Describe("a pending 0001 fill submitted again after it retired before the read", retired)); + RequireStored(Dwords, Uniform(0x40), 0, "the 0001 fill submitted again"); + + const auto other = SubmitAndRead(UserData(Dwords, Dwords, Uniform(0x5a)), Dwords / 64); + Require(other.keys == DccKeys::Mixed && other.waited, Describe("a pending fill of a byte that is not a key code", other)); + RequireStored(Dwords, Uniform(0x5a), 0, "the kernel's fill of another byte"); + + const Pattern several{0x40404040u, 0x40404040u, 0x40404040u, 0x80808080u}; + const auto mixed = SubmitAndRead(UserData(Dwords, Dwords, several), Dwords / 64); + Require(mixed.keys == DccKeys::Mixed && mixed.waited, Describe("a pending fill of several key codes", mixed)); + RequireStored(Dwords, several, 0, "the kernel's fill of several key codes"); + + const auto first = SubmitAndRead(UserData(Dwords, Dwords, Uniform(0x40)), Dwords / 64); + Require(first.keys == DccKeys::Clear0001 && !first.waited, Describe("a pending 0001 fill by the kernel", first)); + const auto later = SubmitAndRead(UserData(Dwords, Dwords / 2, Uniform(0x5a)), Dwords / 64); + Require(later.keys == DccKeys::Mixed && later.waited, Describe("a later writer over half of a pending 0001 fill", later)); + Settle(); + for (std::uint32_t i = 0; i < Dwords; ++i) Require(Keys[i] == (i < Dwords / 2 ? 0x5a5a5a5au : 0x40404040u), "the later writer over the 0001 fill: dword " + std::to_string(i) + " holds " + std::to_string(Keys[i])); + + const auto half = SubmitAndRead(UserData(Dwords, Dwords / 2, Uniform(0xc0)), Dwords / 64); + Require(half.keys == DccKeys::Mixed, Describe("a pending 1111 fill of half the keys over 0001 keys", half)); + Settle(); + const auto inside = SubmitAndRead(UserData(Dwords, Dwords / 2, Uniform(0xc0)), Dwords / 64, SurfaceBytes / 2); + Require(inside.keys == DccKeys::Clear1111, Describe("the half of the keys a pending 1111 fill covers", inside)); + Settle(); + for (std::uint32_t i = 0; i < Dwords; ++i) Require(Keys[i] == (i < Dwords / 2 ? 0xc0c0c0c0u : 0x40404040u), "the 1111 fill of half the keys: dword " + std::to_string(i) + " holds " + std::to_string(Keys[i])); +} + +} + +int main() { + try { + MatchTests(); + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + ExecuteTests(); + AgcDriverShutdown_nid_postfix(); + std::puts("SGPR pattern fill key tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StagedClearSampledRead.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StagedClearSampledRead.cpp index 8997e9418a..f6016f8995 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/StagedClearSampledRead.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StagedClearSampledRead.cpp @@ -187,7 +187,7 @@ int main() { const auto written = Count(texels, SurfaceBytes, 0xff); const auto cleared = Count(texels, SurfaceBytes, 0x00); char message[320]; - std::snprintf(message, sizeof(message), "the sampled read stored %zu written and %zu cleared bytes of %zu, expected %u written", written, cleared, SurfaceBytes, Threads * 4u + SampleTexelBytes); + std::snprintf(message, sizeof(message), "the sampled read stored %zu written and %zu cleared bytes of %zu, expected %zu written", written, cleared, SurfaceBytes, Threads * 4u + SampleTexelBytes); Require(written == Threads * 4u + SampleTexelBytes && cleared == SurfaceBytes - written, message); for (std::uint32_t index = 0; index < Threads * 4u; ++index) { std::snprintf(message, sizeof(message), "component %u of thread %u is 0x%08x, expected the drawn 0x3f800000 the staged clear does not cover", index % 4u, index / 4u, outputData[index]); 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/StorageFileWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StorageFileWrite.cpp index bd344dd60a..6fd7ebae81 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/StorageFileWrite.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StorageFileWrite.cpp @@ -19,6 +19,10 @@ #include #include #include +#include +#include +#include +#include #include #include #ifdef _WIN32 @@ -256,6 +260,68 @@ bool CheckWrite(Api api, const char* name, GuestSurface& surface, GuestSurface& return differences == 0; } +void CheckConcurrentFlushWait(GuestSurface& surface) { + Produce(surface.Data()); + const auto address = reinterpret_cast(surface.Data()); + Require(AgcDriver::Graphics::StorageTexture::FindPending(address, SurfaceBytes) != nullptr, "file write: concurrent flush setup has no pending image"); + std::mutex mutex; + std::condition_variable changed; + bool gpuLocked = false; + bool releaseGpu = false; + std::thread holder([&] { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + { + std::lock_guard lock(mutex); + gpuLocked = true; + } + changed.notify_all(); + std::unique_lock lock(mutex); + changed.wait(lock, [&] { return releaseGpu; }); + }); + { + std::unique_lock lock(mutex); + changed.wait(lock, [&] { return gpuLocked; }); + } + std::exception_ptr flushFailure; + std::thread flusher([&] { + try { AgcDriver::Graphics::StorageTexture::FlushPending(address, SurfaceBytes, nullptr, "concurrent flush test"); } + catch (...) { flushFailure = std::current_exception(); } + }); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (AgcDriver::Graphics::StorageTexture::FindPending(address, SurfaceBytes) != nullptr && std::chrono::steady_clock::now() < deadline) std::this_thread::yield(); + const bool taken = AgcDriver::Graphics::StorageTexture::FindPending(address, SurfaceBytes) == nullptr; + std::vector seen(SurfaceBytes); + std::atomic readDone = false; + std::exception_ptr readFailure; + std::thread reader; + if (taken) { + reader = std::thread([&] { + try { + AgcDriver::Graphics::StorageTexture::FlushPending(address, SurfaceBytes, nullptr, "concurrent read test"); + AgcDriver::GuestMemory::Read(address, seen); + } catch (...) { + readFailure = std::current_exception(); + } + readDone.store(true, std::memory_order_release); + changed.notify_all(); + }); + std::unique_lock lock(mutex); + changed.wait_for(lock, std::chrono::milliseconds(50), [&] { return readDone.load(std::memory_order_acquire); }); + releaseGpu = true; + } else { + std::lock_guard lock(mutex); + releaseGpu = true; + } + changed.notify_all(); + holder.join(); + flusher.join(); + if (reader.joinable()) reader.join(); + Require(taken, "file write: concurrent flush did not take the pending image"); + if (flushFailure != nullptr) std::rethrow_exception(flushFailure); + if (readFailure != nullptr) std::rethrow_exception(readFailure); + Require(std::any_of(seen.begin(), seen.end(), [](std::byte value) { return value != std::byte{}; }), "file write: concurrent guest read returned before an overlapping image was stored"); +} + void CheckShutdownAndGuard() { OutputFile file; std::vector source(TransferBytes, std::byte{0x5a}); @@ -282,6 +348,7 @@ int main() { } GuestSurface surface; GuestSurface unrelated; + CheckConcurrentFlushWait(surface); Produce(unrelated.Data()); bool correct = true; const std::array, 12> cases{{ diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StorageReadDuringIdleStore.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StorageReadDuringIdleStore.cpp new file mode 100644 index 0000000000..ece9a4e874 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StorageReadDuringIdleStore.cpp @@ -0,0 +1,159 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif + +namespace { + +using AgcDriver::Graphics::Require; +using AgcDriver::Graphics::StorageTexture; +constexpr std::uint32_t Width = 128; +constexpr std::uint32_t Height = 120; +constexpr std::size_t SurfaceBytes = 0x20000; +constexpr int Rounds = 20; +alignas(256) constexpr std::array WriteCode{ + 0xd7460004u, 0x0401060eu, 0xd7460005u, 0x0405060fu, 0x7e100d04u, 0x7e120d05u, + 0x7e140c0du, 0x7e1602f2u, 0xf0200f08u, 0x00000804u, 0xbf810000u, +}; + +class GuestSurface { +public: + GuestSurface() { +#ifdef _WIN32 + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(SurfaceBytes, SurfaceBytes)); + GuestArena::GuestArenaCommit_nid_postfix(block, SurfaceBytes, PAGE_READWRITE, SurfaceBytes); +#else + void* mapped = mmap(nullptr, SurfaceBytes * 2, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(mapped != MAP_FAILED, "idle store read: cannot allocate the guest surface"); + const auto begin = reinterpret_cast(mapped); + const auto aligned = (begin + SurfaceBytes - 1) & ~(std::uintptr_t{SurfaceBytes - 1}); + if (aligned != begin) munmap(mapped, aligned - begin); + const auto tail = begin + SurfaceBytes * 2 - aligned - SurfaceBytes; + if (tail != 0) munmap(reinterpret_cast(aligned + SurfaceBytes), tail); + block = reinterpret_cast(aligned); + if (AgcDriver::GuestMemory::WriteWatched()) GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, SurfaceBytes); +#endif + GuestAllocations::Mutation mutation; + mutation.Add(block, SurfaceBytes, true, true, true); + } + + ~GuestSurface() { + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(block, SurfaceBytes); + GuestArena::GuestArenaRelease_nid_postfix(block, SurfaceBytes); +#else + munmap(block, SurfaceBytes); + if (AgcDriver::GuestMemory::WriteWatched()) GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, SurfaceBytes); +#endif + } + + GuestSurface(const GuestSurface&) = delete; + GuestSurface& operator=(const GuestSurface&) = delete; + std::uint8_t* Data() { return block; } + std::uint64_t Address() const { return reinterpret_cast(block); } + +private: + std::uint8_t* block = nullptr; +}; + +void Produce(GuestSurface& surface) { + std::memset(surface.Data(), 0, SurfaceBytes); + const auto address = surface.Address(); + const std::array data{ + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (71u << 20u) | (((Width - 1u) & 3u) << 30u), + ((Width - 1u) >> 2u) | ((Height - 1u) << 14u), + 0xfacu | (0x09u << 20u) | (9u << 28u), + 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 7u, + }; + 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{8, 8, 1}; + registers(0x207, shape); + const auto code = reinterpret_cast(WriteCode.data()); + const std::array program{static_cast(code >> 8u), static_cast(code >> 40u)}; + registers(0x20c, program); + const std::array resource{(static_cast(data.size()) << 1u) | (7u << 7u) | (1u << 11u)}; + registers(0x213, resource); + registers(0x240, data); + words.insert(words.end(), {0xc0031500u, Width / 8u, Height / 8u, 1u, 0x41u}); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + AgcDriverSuspendPoint_nid_postfix(); + AgcDriverWaitIdle_nid_postfix(); + Require(StorageTexture::FindPending(surface.Address(), SurfaceBytes) != nullptr, "idle store read: the GPU producer left no pending image"); +} + +bool AllZero(std::span bytes) { + return std::none_of(bytes.begin(), bytes.end(), [](std::byte value) { return value != std::byte{}; }); +} + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + GuestSurface surface; + int stale = 0; + for (int round = 0; round < Rounds; ++round) { + Produce(surface); + std::atomic started{false}; + std::thread idle([&] { + started = true; + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + StorageTexture::FlushAllPending("idle"); + }); + while (!started) {} + while (StorageTexture::FindPending(surface.Address(), SurfaceBytes) != nullptr) {} + std::vector seen(SurfaceBytes); + AgcDriver::GuestMemory::Read(surface.Address(), seen); + idle.join(); + stale += AllZero(seen); + std::vector stored(SurfaceBytes); + AgcDriver::GuestMemory::Read(surface.Address(), stored); + Require(!AllZero(stored), "idle store read: the GPU producer wrote only zeros"); + } + std::printf("idle store read: %d of %d host reads saw the memory before the store\n", stale, Rounds); + Require(stale == 0, "idle store read: a host read did not wait for the image another thread was storing"); + AgcDriverShutdown_nid_postfix(); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << "\n"; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StorageWriteBackNoWindow.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StorageWriteBackNoWindow.cpp new file mode 100644 index 0000000000..acde6de596 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StorageWriteBackNoWindow.cpp @@ -0,0 +1,179 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::DescribeSurface; +using AgcDriver::Graphics::GuestTextureResource; +using AgcDriver::Graphics::Require; +using AgcDriver::Graphics::StorageTexture; +using AgcDriver::Graphics::TextureDetiler; +using AgcDriver::Graphics::TextureDimension; +using AgcDriver::Graphics::TextureTileMode; + +// A thin 3D surface (SW_64KB_R_X, the branch a volume keeps its depth slices in) whose mip tail +// starts at level 1: with 16 depth slices, level 1's blocks exist for the first 8 slices only +// (HasLayer's taper) while the flat 64 KiB unit grid spans all 16. The first 64 KiB unit of every +// slice past the taper start therefore lies outside every (slice, level) window the write-back +// retiles through, and slices 0..7 have both their units inside windows. +constexpr std::uint32_t Width = 128; +constexpr std::uint32_t Height = 128; +constexpr std::uint32_t Depth = 16; +constexpr std::uint32_t MipCount = 5; +constexpr std::uint32_t Format32Float = 22; +constexpr std::size_t UnitBytes = 65536; +constexpr std::uint8_t Sentinel = 0xa5; + +std::uint64_t AddressOf(const void* data) { + return static_cast(reinterpret_cast(data)); +} + +std::string Hex(std::uint64_t value) { + char text[24]; + std::snprintf(text, sizeof(text), "0x%llx", static_cast(value)); + return text; +} + +class GuestBlock { +public: + explicit GuestBlock(std::size_t bytes) : bytes(bytes) { +#ifdef _WIN32 + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, UnitBytes)); + if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, bytes, PAGE_READWRITE, bytes); +#else + void* mapped = mmap(nullptr, bytes + UnitBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapped != MAP_FAILED) { + const auto begin = reinterpret_cast(mapped); + const auto aligned = (begin + UnitBytes - 1) & ~(static_cast(UnitBytes) - 1); + if (aligned != begin) munmap(mapped, aligned - begin); + if (aligned + bytes != begin + bytes + UnitBytes) munmap(reinterpret_cast(aligned + bytes), begin + bytes + UnitBytes - aligned); + block = reinterpret_cast(aligned); + if (AgcDriver::GuestMemory::WriteWatched()) GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, bytes); + } +#endif + Require(block != nullptr, "storage write-back no-window: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block, bytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(block, bytes); + GuestArena::GuestArenaRelease_nid_postfix(block, bytes); +#else + munmap(block, bytes); + if (AgcDriver::GuestMemory::WriteWatched()) GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, bytes); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; + std::size_t bytes = 0; +}; + +void Run(AgcDriver::VulkanDevice& device) { + GuestTextureResource resource{}; + resource.width = Width; + resource.height = Height; + resource.depthOrLastArray = Depth - 1u; + resource.mipCount = MipCount; + resource.tileMode = TextureTileMode::kR64KBX; + resource.dimension = TextureDimension::k3D; + resource.format = Format32Float; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + const auto geometry = DescribeSurface(resource); + Require(geometry.guestBytes >= Depth * UnitBytes, "storage write-back no-window: the fixture's slices are smaller than a 64 KiB unit"); + + GuestBlock block(static_cast(geometry.guestBytes)); + resource.baseAddress = AddressOf(block.Data()); + Require(resource.baseAddress % UnitBytes == 0, "storage write-back no-window: the guest block is not 64 KiB aligned"); + std::memset(block.Data(), Sentinel, static_cast(geometry.guestBytes)); + AgcDriver::GuestMemory::CollectWrites(resource.baseAddress, static_cast(geometry.guestBytes)); + AgcDriver::GuestMemory::BumpCollectEpoch(); + + auto context = device.graphicsContext(); + TextureDetiler detiler(context); + context.detiler = &detiler; + const auto texture = StorageTexture::Create(context, detiler, resource, 0); + texture->MarkDirty(); + Require(StorageTexture::FindPending(resource.baseAddress, geometry.guestBytes) != nullptr, "storage write-back no-window: the marked image is not pending"); + + // The (slice, level) windows sliceWindows would retile a kept unit through: the mip levels of + // every depth slice HasLayer keeps, each at its tiled offset in the slice. + std::vector> windows; + for (std::uint32_t layer = 0; layer < geometry.layers; ++layer) { + for (std::uint32_t level = 0; level < geometry.mips.size(); ++level) { + if (!geometry.HasLayer(level, layer)) continue; + const auto begin = resource.baseAddress + geometry.GuestLayerOffset(layer) + geometry.mips[level].tiledOffset; + windows.emplace_back(begin, begin + geometry.mips[level].tiledSize); + } + } + Require(!windows.empty(), "storage write-back no-window: the fixture has no slice windows"); + + // Every 64 KiB unit of the flat tracking grid is stored on its own: a unit inside a window + // stores through the retiler, one outside every window must store nothing and leave the guest + // bytes alone instead of sizing the retiler's buffers at zero. + std::size_t units = 0, outside = 0; + for (auto begin = resource.baseAddress; begin < resource.baseAddress + geometry.guestBytes; begin += UnitBytes) { + const auto end = std::min(begin + UnitBytes, resource.baseAddress + geometry.guestBytes); + const auto bytes = static_cast(end - begin); + const bool inWindow = std::any_of(windows.begin(), windows.end(), [&](const std::pair& window) { return begin < window.second && window.first < end; }); + Require(StorageTexture::FlushPending(begin, bytes, nullptr, "test"), "storage write-back no-window: the unit at +" + Hex(begin - resource.baseAddress) + " was not listed by its flush"); + device.WaitIdle(); + Require(!StorageTexture::FlushPending(begin, bytes, nullptr, "test"), "storage write-back no-window: the unit at +" + Hex(begin - resource.baseAddress) + " stayed pending after its flush"); + ++units; + if (inWindow) continue; + ++outside; + const bool untouched = std::all_of(block.Data() + (begin - resource.baseAddress), block.Data() + (end - resource.baseAddress), [](std::uint8_t byte) { return byte == Sentinel; }); + Require(untouched, "storage write-back no-window: the flush of the unit at +" + Hex(begin - resource.baseAddress) + ", which lies outside every slice window, wrote to its guest bytes"); + } + Require(outside != 0, "storage write-back no-window: the thin volume fixture has no unit outside every slice window"); + Require(!texture->Dirty(), "storage write-back no-window: flushing every unit left the image pending"); + std::printf("storage write-back no-window tests passed (%zu units, %zu outside every window)\n", units, outside); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Run(*device); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StorageWriteBackOverhang.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StorageWriteBackOverhang.cpp index 4f5f7f8c0b..9dd13a3b30 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/StorageWriteBackOverhang.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StorageWriteBackOverhang.cpp @@ -75,6 +75,7 @@ void Write(std::uint8_t* texels) { AgcDriverWaitIdle_nid_postfix(); AgcDriver::Graphics::StorageTexture::FlushAllPending("test"); AgcDriverWaitIdle_nid_postfix(); + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(texels), SurfaceBytes); } class GuestSurface { 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/libSceAgcDriver/tests/execution/Tessellation.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp index 9ed2c5c32c..87c935f0d2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp @@ -118,6 +118,14 @@ alignas(256) constexpr std::array DomainCode{ 0x7e2802f2, 0xf80008cf, 0x14131110, 0xf800020f, 0x14131214, 0xbf810000, }; +alignas(256) constexpr std::array DomainComputedPoint{ + 0xf8000941, 0x00000000, 0x34020e84, 0x16020283, 0xe0341000, 0x04030a01, 0xbf8c3f70, 0x7c0214ff, + 0x7f7fffff, 0xd5010002, 0x01a90280, 0xd7460002, 0x04050902, 0xe0341010, 0x04030c02, 0xe0341020, + 0x04030e02, 0xbf8c3f70, 0x08040af2, 0x08040d02, 0x10201505, 0x3e201906, 0x3e201d02, 0x10221705, + 0x3e221b06, 0x3e221f02, 0xd5510012, 0x040a0d05, 0x102424ff, 0x40400000, 0x7e260280, 0x7e2802f2, + 0xf80008cf, 0x14131110, 0xf800020f, 0x14131214, 0xbf810000, +}; + alignas(256) constexpr std::array DomainLoweredInterior{ 0xf8000941, 0x00000000, 0x34020e84, 0x16020283, 0xe0341000, 0x04030a01, 0xe0341010, 0x04030c01, 0xe0341020, 0x04030e01, 0xbf8c3f70, 0x08040af2, 0x08040d02, 0x10201505, 0x3e201906, 0x3e201d02, @@ -326,6 +334,90 @@ alignas(256) constexpr std::array QuadDomainCode{ 0x7e3402f2, 0xf80008cf, 0x1a191716, 0xf800020f, 0x1a19181a, 0xbf810000, }; +alignas(256) constexpr std::array MergedHullCode{ + 0xbefe04c1, 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0x7da206f9, 0x80060000, 0xbf88001c, + 0xd5480004, 0x02090502, 0xd5480005, 0x02090102, 0xd5480006, 0x02110902, 0xd5480007, 0x02111102, + 0x7e080d04, 0x7e0a0d05, 0x7e0c0d06, 0x7e0e0d07, 0x10100cff, 0x3f19999a, 0x061010ff, 0xbf666666, + 0x3e1008ff, 0x3e800000, 0xd54b0009, 0x03c5e105, 0x161406a0, 0xd9340000, 0x0000080a, 0xd8340008, + 0x0000070a, 0xd8340010, 0x0000070a, 0xbf8cc07f, 0xbefe04c1, 0xbf8cc07f, 0xd7650000, 0x000100c1, + 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, 0x02151101, 0x362402ff, + 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, 0x02000015, 0x162c2890, + 0xd5430016, 0x045a24ff, 0x00000030, 0xbf8cc07f, 0xe0781000, 0x02020216, 0x7da82881, 0xbe94047e, + 0xb06a0060, 0x162602f9, 0x0086066a, 0xd8d80048, 0x06000013, 0xd8dc1008, 0x07000013, 0xbf8cc07f, + 0x7e300306, 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, 0xbefe0414, 0xe0781000, 0x0403180a, + 0x4a162481, 0x16161690, 0x4c1616ff, 0x00008000, 0xe0741000, 0x0202070b, 0xbf810000, +}; + +alignas(256) constexpr std::array MergedHullRegisters{ + 0xbefe04c1, 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0x7da206f9, 0x80060000, 0xbf88001c, + 0xd5480004, 0x02090502, 0xd5480005, 0x02090102, 0xd5480006, 0x02110902, 0xd5480007, 0x02111102, + 0x7e080d04, 0x7e0a0d05, 0x7e0c0d06, 0x7e0e0d07, 0x10100cff, 0x3f19999a, 0x061010ff, 0xbf666666, + 0x3e1008ff, 0x3e800000, 0xd54b0009, 0x03c5e105, 0x161406a0, 0xd9340000, 0x0000080a, 0xd8340008, + 0x0000070a, 0xd8340010, 0x0000070a, 0xbf8cc07f, 0xbefe04c1, 0xbf8cc07f, 0xd7650000, 0x000100c1, + 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, 0x02151101, 0x362402ff, + 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, 0x02000015, 0x162c2890, + 0xd5430016, 0x045a24ff, 0x00000030, 0xbf8cc07f, 0xe0741000, 0x02020816, 0x7da82881, 0xbe94047e, + 0xb06a0060, 0x162602f9, 0x0086066a, 0xd8d80048, 0x06000013, 0xd8dc1008, 0x07000013, 0xbf8cc07f, + 0x7e300306, 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, 0xbefe0414, 0xe0781000, 0x0403180a, + 0x4a162481, 0x16161690, 0x4c1616ff, 0x00008000, 0xe0741000, 0x0202070b, 0xbf810000, +}; + +alignas(256) constexpr std::array MergedHullForeignRecord{ + 0xbefe04c1, 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0x7da206f9, 0x80060000, 0xbf88001c, + 0xd5480004, 0x02090502, 0xd5480005, 0x02090102, 0xd5480006, 0x02110902, 0xd5480007, 0x02111102, + 0x7e080d04, 0x7e0a0d05, 0x7e0c0d06, 0x7e0e0d07, 0x10100cff, 0x3f19999a, 0x061010ff, 0xbf666666, + 0x3e1008ff, 0x3e800000, 0xd54b0009, 0x03c5e105, 0x161406a0, 0xd9340020, 0x0000080a, 0xd8340008, + 0x0000070a, 0xd8340010, 0x0000070a, 0xbf8cc07f, 0xbefe04c1, 0xbf8cc07f, 0xd7650000, 0x000100c1, + 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, 0x02151101, 0x362402ff, + 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, 0x02000015, 0x162c2890, + 0xd5430016, 0x045a24ff, 0x00000030, 0xbf8cc07f, 0xe0781000, 0x02020216, 0x7da82881, 0xbe94047e, + 0xb06a0060, 0x162602f9, 0x0086066a, 0xd8d80048, 0x06000013, 0xd8dc1008, 0x07000013, 0xbf8cc07f, + 0x7e300306, 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, 0xbefe0414, 0xe0781000, 0x0403180a, + 0x4a162481, 0x16161690, 0x4c1616ff, 0x00008000, 0xe0741000, 0x0202070b, 0xbf810000, +}; + +alignas(256) constexpr std::array MergedHullDivergentBarrier{ + 0xbefe04c1, 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0x7da206f9, 0x80060000, 0xbf88001d, + 0xd5480004, 0x02090502, 0xd5480005, 0x02090102, 0xd5480006, 0x02110902, 0xd5480007, 0x02111102, + 0x7e080d04, 0x7e0a0d05, 0x7e0c0d06, 0x7e0e0d07, 0x10100cff, 0x3f19999a, 0x061010ff, 0xbf666666, + 0x3e1008ff, 0x3e800000, 0xd54b0009, 0x03c5e105, 0x161406a0, 0xd9340000, 0x0000080a, 0xd8340008, + 0x0000070a, 0xd8340010, 0x0000070a, 0xbf8a0000, 0xbf8cc07f, 0xbefe04c1, 0xbf8cc07f, 0xd7650000, + 0x000100c1, 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, 0x02151101, + 0x362402ff, 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, 0x02000015, + 0x162c2890, 0xd5430016, 0x045a24ff, 0x00000030, 0xbf8cc07f, 0xe0781000, 0x02020216, 0x7da82881, + 0xbe94047e, 0xb06a0060, 0x162602f9, 0x0086066a, 0xd8d80048, 0x06000013, 0xd8dc1008, 0x07000013, + 0xbf8cc07f, 0x7e300306, 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, 0xbefe0414, 0xe0781000, + 0x0403180a, 0x4a162481, 0x16161690, 0x4c1616ff, 0x00008000, 0xe0741000, 0x0202070b, 0xbf810000, +}; + +alignas(256) constexpr std::array MergedHullOutsidePatch{ + 0xbefe04c1, 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0x7da206f9, 0x80060000, 0xbf88001c, + 0xd5480004, 0x02090502, 0xd5480005, 0x02090102, 0xd5480006, 0x02110902, 0xd5480007, 0x02111102, + 0x7e080d04, 0x7e0a0d05, 0x7e0c0d06, 0x7e0e0d07, 0x10100cff, 0x3f19999a, 0x061010ff, 0xbf666666, + 0x3e1008ff, 0x3e800000, 0xd54b0009, 0x03c5e105, 0x161406a0, 0xd9340000, 0x0000080a, 0xd8340008, + 0x0000070a, 0xd8340010, 0x0000070a, 0xbf8cc07f, 0xbefe04c1, 0xbf8cc07f, 0xd7650000, 0x000100c1, + 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, 0x02151101, 0x362402ff, + 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, 0x02000015, 0x162c2890, + 0xd5430016, 0x045a24ff, 0x00000030, 0xbf8cc07f, 0xe0781000, 0x02020216, 0x7da82881, 0xbe94047e, + 0xb06a0060, 0x162602f9, 0x0086066a, 0xd8d80060, 0x06000013, 0xd8dc1008, 0x07000013, 0xbf8cc07f, + 0x7e300306, 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, 0xbefe0414, 0xe0781000, 0x0403180a, + 0x4a162481, 0x16161690, 0x4c1616ff, 0x00008000, 0xe0741000, 0x0202070b, 0xbf810000, +}; + +alignas(256) constexpr std::array MergedHullReadLane{ + 0xbefe04c1, 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0x7da206f9, 0x80060000, 0xbf88001c, + 0xd5480004, 0x02090502, 0xd5480005, 0x02090102, 0xd5480006, 0x02110902, 0xd5480007, 0x02111102, + 0x7e080d04, 0x7e0a0d05, 0x7e0c0d06, 0x7e0e0d07, 0x10100cff, 0x3f19999a, 0x061010ff, 0xbf666666, + 0x3e1008ff, 0x3e800000, 0xd54b0009, 0x03c5e105, 0x161406a0, 0xd9340000, 0x0000080a, 0xd8340008, + 0x0000070a, 0xd8340010, 0x0000070a, 0xbf8cc07f, 0xbefe04c1, 0xbf8cc07f, 0xd7650000, 0x000100c1, + 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, 0x02151101, 0x362402ff, + 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, 0x02000015, 0x162c2890, + 0xd5430016, 0x045a24ff, 0x00000030, 0xbf8cc07f, 0xe0781000, 0x02020216, 0x7da82881, 0xbe94047e, + 0xb06a0060, 0x162602f9, 0x0086066a, 0xd8d80048, 0x06000013, 0xd8dc1008, 0x07000013, 0xbf8cc07f, + 0xd7600010, 0x02010106, 0x7e300210, 0x7e360306, 0x34142484, 0xbefe0414, 0xe0781000, 0x0403180a, + 0x4a162481, 0x16161690, 0x4c1616ff, 0x00008000, 0xe0741000, 0x0202070b, 0xbf810000, +}; + alignas(256) constexpr std::array PixelCode{ 0xc8020002, 0xc8060102, 0xc80a0202, 0xc80e0302, 0xf800180f, 0x03020100, 0xbf810000, }; @@ -333,6 +425,7 @@ alignas(256) constexpr std::array PixelCode{ using ShaderRecompiler::TessellationConfiguration; constexpr TessellationConfiguration Patches{3u, 3u, 1u, 2u, 2u}; +constexpr TessellationConfiguration MergedPatches{3u, 3u, 1u, 2u, 2u, 256u, 0u, false, 0u, false, true}; constexpr std::array PatchLevels{1u, 0u, 5u}; constexpr std::uint16_t Corner(std::uint32_t patch, std::uint32_t x, std::uint32_t y) { @@ -379,6 +472,7 @@ alignas(256) constexpr std::array NextInputIndices{ const PatchDraw Triangles{Patches, Indices}; const PatchDraw NextInputTriangles{{4u, 3u, 1u, 2u, 2u}, NextInputIndices}; +const PatchDraw MergedTriangles{MergedPatches, Indices}; struct Stages { ShaderRecompiler::RecompileResult local; @@ -504,12 +598,12 @@ std::uint32_t CoveredPixels(std::uint32_t patch, bool interior) { return count; } -void RequirePatches(AgcDriver::VulkanDevice& device, const ShaderRecompiler::SpirvTarget& target, std::span hull, const std::string& program, const PatchDraw& patches = Triangles, std::span local = LocalCode) { +void RequirePatches(AgcDriver::VulkanDevice& device, const ShaderRecompiler::SpirvTarget& target, std::span hull, const std::string& program, const PatchDraw& patches = Triangles, std::span local = LocalCode, std::span domain = DomainCode) { std::uint32_t cursor = 0; const Stages stages{ Compile(target, ShaderStage::Local, local, cursor, patches), Compile(target, ShaderStage::TessellationControl, hull, cursor, patches), - Compile(target, ShaderStage::TessellationEvaluation, DomainCode, cursor, patches), + Compile(target, ShaderStage::TessellationEvaluation, domain, cursor, patches), Compile(target, ShaderStage::Fragment, PixelCode, cursor, patches), }; const auto center = Row(-0.5f + 1.0f / 3.0f); @@ -842,6 +936,31 @@ void RequireSingleControlPointHulls(const ShaderRecompiler::SpirvTarget& target) Require(!CompileHull(target, HullSingleOutput, {1u, 1u, 1u, 2u, 2u}).spirv.empty(), "a hull with one output control point and unguarded patch outputs was not compiled"); } +void RequireMergedHullWithoutSubgroups(const ShaderRecompiler::SpirvTarget& target) { + std::uint32_t cursor = 0; + const auto hull = Compile(target, ShaderStage::TessellationControl, MergedHullCode, cursor, MergedTriangles); + const auto words = hull.spirv.Words(); + for (std::size_t at = 5; at + 1u < words.size(); at += words[at] >> 16u) { + Require((words[at] >> 16u) != 0u, "the merged hull program compiled to a SPIR-V instruction without words"); + Require((words[at] & 0xffffu) != spv::OpCapability || words[at + 1u] != spv::CapabilityGroupNonUniform, "the hull stage of a merged hull program declares subgroup operations, which a device without tessellation control subgroups rejects"); + } +} + +void RequireRegisteredMergedStages() { + const auto low = [](const auto& code) { return static_cast(reinterpret_cast(code.data()) >> 8u); }; + const auto high = [](const auto& code) { return static_cast(reinterpret_cast(code.data()) >> 40u); }; + RegisteredStage merged; + RegisteredStage domain; + Describe(merged, MergedHullCode, 3u, {{0x148u, low(MergedHullCode)}, {0x149u, high(MergedHullCode)}, {0x10au, 0u}, {0x10bu, 16u}}, {{0x2dbu, 0x40049u}, {0x286u, 0x42800000u}, {0x287u, 0x3f800000u}, {0x2d6u, 0xc355u}}, 0x105u); + Describe(domain, DomainCode, 2u, {{0xc8u, low(DomainCode)}, {0xc9u, high(DomainCode)}, {0x8au, 0u}, {0x8bu, 16u}}, {}, 0x02002008u); + for (const auto* stage : {&merged, &domain}) AgcDriverRegisterShader_nid_postfix(&stage->shader); + const std::array stages{&merged.shader, &domain.shader}; + const std::array context{{{0x2d5u, 0x0200210du}, {0x29bu, 0u}}}; + const std::array primitive{{{0x25bu, 0u}, {0x262u, 0u}, {0x242u, 9u}}}; + AgcDriverResolveGraphicsStagesAbi_nid_postfix(stages, context, primitive); + AgcDriverResolveGraphicsStagesAbi_nid_postfix(stages, context, primitive); +} + } int main() { @@ -874,9 +993,18 @@ int main() { RequirePatches(*device, target, HullRestoredExecSaved, "hull program saving its restored exec"); RequirePatches(*device, target, HullNextInput, "hull program reading the next of 4 input control points of 24 bytes", NextInputTriangles, LocalCodeStride24); RequirePatches(*device, target, HullDynamicIndex, "hull program reading a control point at a computed index"); + RequirePatches(*device, target, HullCode, "domain program reading control points at a computed index", Triangles, LocalCode, DomainComputedPoint); RequirePatches(*device, target, HullAndSaveexec, "hull program narrowing exec with s_and_saveexec_b64"); RequirePatches(*device, target, HullSaveexecLoop, "hull program restoring exec through loop phis"); RequirePatches(*device, target, HullDescendingRecords, "hull program with descending patch records in 128-patch groups", {{3u, 3u, 1u, 2u, 2u, 128u}, Indices}); + ExpectRefusal(target, ShaderStage::TessellationControl, MergedHullCode, "more input than output control points", {{4u, 3u, 1u, 2u, 2u, 256u, 0u, false, 0u, false, true}, Indices}); + ExpectRefusal(target, ShaderStage::TessellationControl, MergedHullForeignRecord, "outside its own control point's record", MergedTriangles); + ExpectRefusal(target, ShaderStage::TessellationControl, MergedHullDivergentBarrier, "s_barrier that some invocations do not reach", MergedTriangles); + ExpectRefusal(target, ShaderStage::TessellationControl, MergedHullOutsidePatch, "reads LDS outside its patch's control point records", MergedTriangles); + ExpectRefusal(target, ShaderStage::TessellationControl, MergedHullReadLane, "another lane's register", MergedTriangles); + RequireMergedHullWithoutSubgroups(target); + RequirePatches(*device, target, MergedHullCode, "merged hull program", MergedTriangles, MergedHullCode); + RequirePatches(*device, target, MergedHullRegisters, "merged hull program storing its local part's registers", MergedTriangles, MergedHullRegisters); RequireCounterClockwise(*device, target); RequireLevelClamps(*device, target); RequireIntegerPartitioning(*device, target); @@ -888,6 +1016,7 @@ int main() { RequireHullBdaReads(*device, target); RequireCompareBranches(target); device.reset(); + RequireRegisteredMergedStages(); RequireRegisteredStages(); std::puts("Tessellation tests passed"); return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VectorWindowLiveness.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VectorWindowLiveness.cpp new file mode 100644 index 0000000000..54193f052e --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VectorWindowLiveness.cpp @@ -0,0 +1,93 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t MaxThreads = 64; +constexpr std::uint32_t Results = 4; +alignas(256) std::array Output{}; +alignas(256) std::array Unused{}; + +alignas(256) constexpr std::array Code{ + 0x34060084, 0x4a080081, 0xbf068008, 0xbf850001, 0x4a080890, 0xbefc0380, 0x7e028704, 0xe0701000, + 0x80010103, 0x4a080081, 0xbe890383, 0x7e0a0280, 0xbefc0380, 0x7e0c8704, 0x4a0a0b06, 0x4a080890, + 0x80898109, 0xbf078009, 0xbf85fff9, 0xe0701004, 0x80010503, 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::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +void Run(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, std::uint32_t threads, std::uint32_t overwrite) { + Output.fill(0xdeadbeefu); + std::vector userData(9, 0u); + const auto input = BufferDescriptor(Unused.data(), static_cast(Unused.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + userData[8] = overwrite; + 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, {}}, + {waveSize, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(std::uint32_t waveSize, std::uint32_t threads, std::uint32_t overwrite) { + for (std::uint32_t tid = 0; tid < threads; ++tid) { + const std::uint32_t* out = &Output[tid * Results]; + const std::uint32_t written = tid + 1u; + const std::uint32_t branch = overwrite != 0u ? written + 16u : written; + const std::uint32_t loop = 3u * written + 48u; + const auto where = std::string("vector window liveness: wave") + std::to_string(waveSize) + " lane " + std::to_string(tid) + (overwrite != 0u ? " (overwritten before the read)" : " (overwrite branched over)"); + Require(out[0] == branch, where + ": the window register written before the branch read " + Hex(out[0]) + ", expected " + Hex(branch)); + Require(out[1] == loop, where + ": the window register carried around the loop read " + Hex(out[1]) + ", expected " + Hex(loop)); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (const std::uint32_t waveSize : {32u, 64u}) { + for (const std::uint32_t overwrite : {0u, 1u}) { + Run(*device, waveSize, waveSize, overwrite); + Check(waveSize, waveSize, overwrite); + } + } + std::puts("vector window liveness tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Vop3CubeClamp.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Vop3CubeClamp.cpp index 10857e6d31..a26753bd63 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Vop3CubeClamp.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Vop3CubeClamp.cpp @@ -155,9 +155,135 @@ constexpr const char* Names[16] = { "v_cubema_f32 v25, -v4, |v5|, v6 clamp", }; -void Fill(std::uint32_t tid, std::uint32_t* words) { - std::copy(std::begin(Rows[tid]), std::end(Rows[tid]), words); -} +alignas(256) constexpr std::array OmodCode{ + 0x34020084, 0x34060086, 0xe0381000, 0x80000401, 0xbf8c3f70, 0xd544000a, 0x0c1a0b04, 0xd544000b, + 0x1c1a0b04, 0xd545000c, 0x0c1a0b04, 0xd545000d, 0x141a0b04, 0xd545000e, 0x1c1a0b04, 0xd546000f, + 0x0c1a0b04, 0xd5460010, 0x141a0b04, 0xd5460011, 0x1c1a0b04, 0xd5470012, 0x0c1a0b04, 0xd5470013, + 0x141a0b04, 0xd5470014, 0x1c1a0b04, 0xd5440015, 0xdc1a0b04, 0xd5458216, 0x341a0b04, 0xd5468117, + 0x5c1a0b04, 0xd5478018, 0x8c1a0b04, 0xd5440019, 0x141a0b04, 0xe0781000, 0x80010a03, 0xe0781010, + 0x80010e03, 0xe0781020, 0x80011203, 0xe0781030, 0x80011603, 0xbf810000, +}; + +constexpr std::uint32_t OmodRows[32][4] = { + {0x3f800000u, 0x3f000000u, 0x3e800000u, 0x00000000u}, + {0xbf800000u, 0x3f000000u, 0x3e800000u, 0x00000000u}, + {0x3f000000u, 0x3f800000u, 0x3e800000u, 0x00000000u}, + {0x3f000000u, 0xbf800000u, 0xbe800000u, 0x00000000u}, + {0x3e800000u, 0x3f000000u, 0x3f800000u, 0x00000000u}, + {0x3e800000u, 0xbf000000u, 0xbf800000u, 0x00000000u}, + {0x40400000u, 0xc0e00000u, 0x40000000u, 0x00000000u}, + {0xc0200000u, 0x3fc00000u, 0x40c00000u, 0x00000000u}, + {0x7f7fffffu, 0x7f000000u, 0xff000000u, 0x00000000u}, + {0xff7fffffu, 0x7e800000u, 0x7effffffu, 0x00000000u}, + {0x3f800000u, 0x00800000u, 0x80ffffffu, 0x00000000u}, + {0x3f800000u, 0x01000000u, 0x81000000u, 0x00000000u}, + {0x3f800000u, 0x00000007u, 0x80000007u, 0x00000000u}, + {0x3f800000u, 0x80000000u, 0x00000000u, 0x00000000u}, + {0x00800000u, 0x00000000u, 0x80000000u, 0x00000000u}, + {0x00000007u, 0x00000003u, 0x00000001u, 0x00000000u}, + {0x7f800000u, 0x3f800000u, 0x40000000u, 0x00000000u}, + {0xff800000u, 0x7f800000u, 0x3f800000u, 0x00000000u}, + {0x7fc00000u, 0x3f800000u, 0x3f000000u, 0x00000000u}, + {0x3f800000u, 0xffc00000u, 0x3f000000u, 0x00000000u}, + {0x40000000u, 0x3f800000u, 0x7fc00000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x80000000u, 0x80000000u, 0x80000000u, 0x00000000u}, + {0x7149f2cau, 0xf149f2cau, 0x71c9f2cau, 0x00000000u}, + {0x0da24260u, 0x0e224260u, 0x8e736390u, 0x00000000u}, + {0x7e800000u, 0x7e7fffffu, 0x3f800000u, 0x00000000u}, + {0x40000000u, 0x7e800000u, 0x7effffffu, 0x00000000u}, + {0x3f000000u, 0xbf000000u, 0x3f000000u, 0x00000000u}, + {0xc0c5edadu, 0xc08852a3u, 0xc0c18eacu, 0x00000000u}, + {0xbf3d7ad8u, 0xc0737ab3u, 0xc0e5e3c1u, 0x00000000u}, + {0x3f000000u, 0x3e800000u, 0xff812345u, 0x00000000u}, + {0x7f812345u, 0x3f000000u, 0x3e800000u, 0x00000000u} +}; +constexpr std::uint32_t OmodExpected[32][16] = { + {0x00000000u, 0x00000000u, 0xbf000000u, 0xbf800000u, 0xbe000000u, 0xbf800000u, 0xc0000000u, 0xbe800000u, 0x40800000u, 0x41000000u, 0x3f800000u, 0x00000000u, 0x3f800000u, 0x3e800000u, 0x3f800000u, 0x00000000u}, + {0x40000000u, 0x3f000000u, 0x3f000000u, 0x3f800000u, 0x3e000000u, 0xbf800000u, 0xc0000000u, 0xbe800000u, 0xc0800000u, 0xc1000000u, 0xbf800000u, 0x3f000000u, 0x00000000u, 0x3e800000u, 0x00000000u, 0x40800000u}, + {0x40800000u, 0x3f800000u, 0x3f800000u, 0x40000000u, 0x3e800000u, 0x3f000000u, 0x3f800000u, 0x3e000000u, 0x40800000u, 0x41000000u, 0x3f800000u, 0x3fc00000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x41000000u}, + {0x40c00000u, 0x3fc00000u, 0x3f800000u, 0x40000000u, 0x3e800000u, 0x3f000000u, 0x3f800000u, 0x3e000000u, 0xc0800000u, 0xc1000000u, 0xbf800000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x41400000u}, + {0x41000000u, 0x40000000u, 0x3f000000u, 0x3f800000u, 0x3e000000u, 0xbf800000u, 0xc0000000u, 0xbe800000u, 0x40800000u, 0x41000000u, 0x3f800000u, 0x40200000u, 0x00000000u, 0x3e800000u, 0x00000000u, 0x41800000u}, + {0x41200000u, 0x40200000u, 0xbf000000u, 0xbf800000u, 0xbe000000u, 0x3f800000u, 0x40000000u, 0x3e800000u, 0xc0800000u, 0xc1000000u, 0xbf800000u, 0x40000000u, 0x3f800000u, 0x00000000u, 0x3f800000u, 0x41a00000u}, + {0x40c00000u, 0x3fc00000u, 0x40c00000u, 0x41400000u, 0x3fc00000u, 0xc0800000u, 0xc1000000u, 0xbf800000u, 0xc1e00000u, 0xc2600000u, 0xc0e00000u, 0x3f800000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x41400000u}, + {0x41000000u, 0x40000000u, 0xc0a00000u, 0xc1200000u, 0xbfa00000u, 0xc0400000u, 0xc0c00000u, 0xbf400000u, 0x41c00000u, 0x42400000u, 0x40c00000u, 0x40200000u, 0x3f800000u, 0x3f400000u, 0x00000000u, 0x41800000u}, + {0x00000000u, 0x00000000u, 0x7f800000u, 0x7f800000u, 0x7e800000u, 0xff800000u, 0xff800000u, 0xfe800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x3f800000u, 0x00000000u}, + {0x40000000u, 0x3f000000u, 0x7f7fffffu, 0x7f800000u, 0x7e7fffffu, 0xff000000u, 0xff800000u, 0xfe000000u, 0xff800000u, 0xff800000u, 0xff800000u, 0x3f000000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x40800000u}, + {0x00000000u, 0x00000000u, 0x017fffffu, 0x01ffffffu, 0x00000000u, 0x81000000u, 0x81800000u, 0x80000000u, 0x40800000u, 0x41000000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x01800000u, 0x02000000u, 0x00800000u, 0x81800000u, 0x82000000u, 0x80800000u, 0x40800000u, 0x41000000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00800000u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40800000u, 0x41000000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40800000u, 0x41000000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x01800000u, 0x02000000u, 0x00800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x01800000u, 0x00000000u}, + {0x41000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x41800000u}, + {0x00000000u, 0x00000000u, 0xc0800000u, 0xc1000000u, 0xbf800000u, 0xc0000000u, 0xc0800000u, 0xbf000000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x00000000u, 0x3f800000u, 0x3f000000u, 0x3f800000u, 0x00000000u}, + {0x40800000u, 0x3f800000u, 0xff800000u, 0xff800000u, 0xff800000u, 0x40000000u, 0x40800000u, 0x3f000000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x3fc00000u, 0x3f800000u, 0x00000000u, 0x3f800000u, 0x41000000u}, + {0x00000000u, 0x00000000u, 0xbf800000u, 0xc0000000u, 0xbe800000u, 0xc0000000u, 0xc0800000u, 0xbf000000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x00000000u, 0x00000000u, 0x3f000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0xbf800000u, 0xc0000000u, 0xbe800000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x40800000u, 0x41000000u, 0x3f800000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0xffc00000u, 0xffc00000u, 0xffc00000u, 0xc0000000u, 0xc0800000u, 0xbf000000u, 0x41000000u, 0x41800000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x3f000000u, 0x3f800000u, 0x00000000u}, + {0x41000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x41800000u}, + {0x41000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x41800000u}, + {0x41000000u, 0x40000000u, 0x71c9f2cau, 0x7249f2cau, 0x70c9f2cau, 0x71c9f2cau, 0x7249f2cau, 0x70c9f2cau, 0x72c9f2cau, 0x7349f2cau, 0x71c9f2cau, 0x40200000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x41800000u}, + {0x41200000u, 0x40200000u, 0x8e224260u, 0x8ea24260u, 0x8d224260u, 0x8ea24260u, 0x8f224260u, 0x8da24260u, 0x8f736390u, 0x8ff36390u, 0x8e736390u, 0x40000000u, 0x0ea24260u, 0x0da24260u, 0x0f736390u, 0x41a00000u}, + {0x00000000u, 0x00000000u, 0xc0000000u, 0xc0800000u, 0xbf000000u, 0xfeffffffu, 0xff7fffffu, 0xfdffffffu, 0x7f800000u, 0x7f800000u, 0x7e800000u, 0x00000000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x00000000u}, + {0x41000000u, 0x40000000u, 0x40800000u, 0x41000000u, 0x3f800000u, 0xff000000u, 0xff800000u, 0xfe000000u, 0x7f800000u, 0x7f800000u, 0x7effffffu, 0x40200000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x41800000u}, + {0x41000000u, 0x40000000u, 0x3f800000u, 0x40000000u, 0x3e800000u, 0x3f800000u, 0x40000000u, 0x3e800000u, 0x40000000u, 0x40800000u, 0x3f000000u, 0x40200000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x41800000u}, + {0x40000000u, 0x3f000000u, 0xc1418eacu, 0xc1c18eacu, 0xc0418eacu, 0x410852a3u, 0x418852a3u, 0x400852a3u, 0xc1c5edadu, 0xc245edadu, 0xc0c5edadu, 0x3f000000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x40800000u}, + {0x41200000u, 0x40200000u, 0x3fbd7ad8u, 0x403d7ad8u, 0x3ebd7ad8u, 0x40f37ab3u, 0x41737ab3u, 0x3ff37ab3u, 0xc1e5e3c1u, 0xc265e3c1u, 0xc0e5e3c1u, 0x40000000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x41a00000u}, + {0x00000000u, 0x00000000u, 0x7f812345u, 0x7f812345u, 0x7f812345u, 0xbf000000u, 0xbf800000u, 0xbe000000u, 0x40000000u, 0x40800000u, 0x3f000000u, 0x00000000u, 0x00000000u, 0x3e000000u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0xbf000000u, 0xbf800000u, 0xbe000000u, 0xbf800000u, 0xc0000000u, 0xbe800000u, 0x7f812345u, 0x7f812345u, 0x7f812345u, 0x00000000u, 0x00000000u, 0x3e800000u, 0x00000000u, 0x00000000u} +}; +constexpr std::uint32_t OmodExpectedIeee[32][16] = { + {0x00000000u, 0x00000000u, 0xbe800000u, 0xbe800000u, 0xbe800000u, 0xbf000000u, 0xbf000000u, 0xbf000000u, 0x40000000u, 0x40000000u, 0x40000000u, 0x00000000u, 0x3e800000u, 0x3f000000u, 0x3f800000u, 0x00000000u}, + {0x3f800000u, 0x3f800000u, 0x3e800000u, 0x3e800000u, 0x3e800000u, 0xbf000000u, 0xbf000000u, 0xbf000000u, 0xc0000000u, 0xc0000000u, 0xc0000000u, 0x3f800000u, 0x00000000u, 0x3f000000u, 0x00000000u, 0x3f800000u}, + {0x40000000u, 0x40000000u, 0x3f000000u, 0x3f000000u, 0x3f000000u, 0x3e800000u, 0x3e800000u, 0x3e800000u, 0x40000000u, 0x40000000u, 0x40000000u, 0x40400000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x40000000u}, + {0x40400000u, 0x40400000u, 0x3f000000u, 0x3f000000u, 0x3f000000u, 0x3e800000u, 0x3e800000u, 0x3e800000u, 0xc0000000u, 0xc0000000u, 0xc0000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40400000u}, + {0x40800000u, 0x40800000u, 0x3e800000u, 0x3e800000u, 0x3e800000u, 0xbf000000u, 0xbf000000u, 0xbf000000u, 0x40000000u, 0x40000000u, 0x40000000u, 0x40a00000u, 0x00000000u, 0x3f000000u, 0x00000000u, 0x40800000u}, + {0x40a00000u, 0x40a00000u, 0xbe800000u, 0xbe800000u, 0xbe800000u, 0x3f000000u, 0x3f000000u, 0x3f000000u, 0xc0000000u, 0xc0000000u, 0xc0000000u, 0x40800000u, 0x3e800000u, 0x00000000u, 0x3f800000u, 0x40a00000u}, + {0x40400000u, 0x40400000u, 0x40400000u, 0x40400000u, 0x40400000u, 0xc0000000u, 0xc0000000u, 0xc0000000u, 0xc1600000u, 0xc1600000u, 0xc1600000u, 0x40000000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x40400000u}, + {0x40800000u, 0x40800000u, 0xc0200000u, 0xc0200000u, 0xc0200000u, 0xbfc00000u, 0xbfc00000u, 0xbfc00000u, 0x41400000u, 0x41400000u, 0x41400000u, 0x40a00000u, 0x3f800000u, 0x3f800000u, 0x00000000u, 0x40800000u}, + {0x00000000u, 0x00000000u, 0x7f000000u, 0x7f000000u, 0x7f000000u, 0xff000000u, 0xff000000u, 0xff000000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x3f800000u, 0x00000000u}, + {0x3f800000u, 0x3f800000u, 0x7effffffu, 0x7effffffu, 0x7effffffu, 0xfe800000u, 0xfe800000u, 0xfe800000u, 0xff800000u, 0xff800000u, 0xff800000u, 0x3f800000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x3f800000u}, + {0x00000000u, 0x00000000u, 0x00ffffffu, 0x00ffffffu, 0x00ffffffu, 0x80800000u, 0x80800000u, 0x80800000u, 0x40000000u, 0x40000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x00800000u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x01000000u, 0x01000000u, 0x01000000u, 0x81000000u, 0x81000000u, 0x81000000u, 0x40000000u, 0x40000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x01000000u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000007u, 0x00000007u, 0x00000007u, 0x80000007u, 0x80000007u, 0x80000007u, 0x40000000u, 0x40000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x00000007u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40000000u, 0x40000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x01000000u, 0x01000000u, 0x01000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x01000000u, 0x00000000u}, + {0x40800000u, 0x40800000u, 0x00000007u, 0x00000007u, 0x00000007u, 0x80000003u, 0x80000003u, 0x80000003u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40800000u, 0x00000000u, 0x00000003u, 0x00000000u, 0x40800000u}, + {0x00000000u, 0x00000000u, 0xc0000000u, 0xc0000000u, 0xc0000000u, 0xbf800000u, 0xbf800000u, 0xbf800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x00000000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x00000000u}, + {0x40000000u, 0x40000000u, 0xff800000u, 0xff800000u, 0xff800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x40400000u, 0x3f800000u, 0x00000000u, 0x3f800000u, 0x40000000u}, + {0x00000000u, 0x00000000u, 0xbf000000u, 0xbf000000u, 0xbf000000u, 0xbf800000u, 0xbf800000u, 0xbf800000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0xbf000000u, 0xbf000000u, 0xbf000000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x40000000u, 0x40000000u, 0x40000000u, 0x00000000u, 0x3f000000u, 0x00000000u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0xffc00000u, 0xffc00000u, 0xffc00000u, 0xbf800000u, 0xbf800000u, 0xbf800000u, 0x40800000u, 0x40800000u, 0x40800000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x3f800000u, 0x00000000u}, + {0x40800000u, 0x40800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40800000u}, + {0x40800000u, 0x40800000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40800000u}, + {0x40800000u, 0x40800000u, 0x7149f2cau, 0x7149f2cau, 0x7149f2cau, 0x7149f2cau, 0x7149f2cau, 0x7149f2cau, 0x7249f2cau, 0x7249f2cau, 0x7249f2cau, 0x40a00000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40800000u}, + {0x40a00000u, 0x40a00000u, 0x8da24260u, 0x8da24260u, 0x8da24260u, 0x8e224260u, 0x8e224260u, 0x8e224260u, 0x8ef36390u, 0x8ef36390u, 0x8ef36390u, 0x40800000u, 0x0da24260u, 0x0e224260u, 0x0ef36390u, 0x40a00000u}, + {0x00000000u, 0x00000000u, 0xbf800000u, 0xbf800000u, 0xbf800000u, 0xfe7fffffu, 0xfe7fffffu, 0xfe7fffffu, 0x7f000000u, 0x7f000000u, 0x7f000000u, 0x00000000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x00000000u}, + {0x40800000u, 0x40800000u, 0x40000000u, 0x40000000u, 0x40000000u, 0xfe800000u, 0xfe800000u, 0xfe800000u, 0x7f7fffffu, 0x7f7fffffu, 0x7f7fffffu, 0x40a00000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x40800000u}, + {0x40800000u, 0x40800000u, 0x3f000000u, 0x3f000000u, 0x3f000000u, 0x3f000000u, 0x3f000000u, 0x3f000000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x40a00000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40800000u}, + {0x3f800000u, 0x3f800000u, 0xc0c18eacu, 0xc0c18eacu, 0xc0c18eacu, 0x408852a3u, 0x408852a3u, 0x408852a3u, 0xc145edadu, 0xc145edadu, 0xc145edadu, 0x3f800000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x3f800000u}, + {0x40a00000u, 0x40a00000u, 0x3f3d7ad8u, 0x3f3d7ad8u, 0x3f3d7ad8u, 0x40737ab3u, 0x40737ab3u, 0x40737ab3u, 0xc165e3c1u, 0xc165e3c1u, 0xc165e3c1u, 0x40800000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x40a00000u}, + {0x00000000u, 0x00000000u, 0x7fc12345u, 0x7fc12345u, 0x7fc12345u, 0xbe800000u, 0xbe800000u, 0xbe800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x3e800000u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0xbe800000u, 0xbe800000u, 0xbe800000u, 0xbf000000u, 0xbf000000u, 0xbf000000u, 0x7fc12345u, 0x7fc12345u, 0x7fc12345u, 0x00000000u, 0x00000000u, 0x3f000000u, 0x00000000u, 0x00000000u} +}; +constexpr const char* OmodNames[16] = { + "v_cubeid_f32 v10, v4, v5, v6 mul:2", + "v_cubeid_f32 v11, v4, v5, v6 div:2", + "v_cubesc_f32 v12, v4, v5, v6 mul:2", + "v_cubesc_f32 v13, v4, v5, v6 mul:4", + "v_cubesc_f32 v14, v4, v5, v6 div:2", + "v_cubetc_f32 v15, v4, v5, v6 mul:2", + "v_cubetc_f32 v16, v4, v5, v6 mul:4", + "v_cubetc_f32 v17, v4, v5, v6 div:2", + "v_cubema_f32 v18, v4, v5, v6 mul:2", + "v_cubema_f32 v19, v4, v5, v6 mul:4", + "v_cubema_f32 v20, v4, v5, v6 div:2", + "v_cubeid_f32 v21, v4, -v5, -v6 div:2", + "v_cubesc_f32 v22, -v4, |v5|, v6 clamp mul:4", + "v_cubetc_f32 v23, |v4|, -v5, v6 clamp div:2", + "v_cubema_f32 v24, v4, v5, -v6 clamp mul:2", + "v_cubeid_f32 v25, v4, v5, v6 mul:4", +}; std::array BufferDescriptor(const void* data, std::uint32_t bytes) { const auto address = reinterpret_cast(data); @@ -193,33 +319,19 @@ ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std:: return ShaderRecompiler::Recompile(request); } -void Run(AgcDriver::VulkanDevice& device, const std::optional& floatMode) { - for (std::uint32_t tid = 0; tid < Threads; ++tid) Fill(tid, &Input[tid * Inputs]); +void Run(AgcDriver::VulkanDevice& device, std::span code, const std::uint32_t (&rows)[32][4], const std::optional& floatMode) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) std::copy(std::begin(rows[tid]), std::end(rows[tid]), &Input[tid * Inputs]); Output.fill(0xdeadbeefu); - const std::span code(Code); const auto result = Compile(device, code, floatMode); device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); device.WaitIdle(); } -void CheckOmodRefused(AgcDriver::VulkanDevice& device) { - for (const std::uint32_t opcode : {0x144u, 0x145u, 0x146u, 0x147u}) { - alignas(256) const std::array code{0xd400000au | (opcode << 16u), 0x0c1a0b04u, 0xbf810000u}; - std::string refusal; - try { - static_cast(Compile(device, code)); - } catch (const std::exception& error) { - refusal = error.what(); - } - Require(refusal.find("VOP3 source modifiers are not implemented") != std::string::npos, "VOP3 opcode " + Hex(opcode) + " with omod was not refused"); - } -} - -void Check(const std::uint32_t (&expected)[32][16], const char* mode) { +void Check(const std::uint32_t (&expected)[32][16], const char* const (&names)[16], const char* mode) { for (std::uint32_t tid = 0; tid < Threads; ++tid) { const std::uint32_t* out = &Output[tid * Results]; for (std::uint32_t i = 0; i < 16; ++i) { - Expect(tid, out[i], expected[tid][i], (std::string(mode) + " " + Names[i]).c_str()); + Expect(tid, out[i], expected[tid][i], (std::string(mode) + " " + names[i]).c_str()); } } } @@ -230,13 +342,22 @@ int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - Run(*device, std::nullopt); - Check(Expected, "no float mode"); - Run(*device, ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}); - Check(Expected, "IEEE=0"); - Run(*device, ShaderRecompiler::ShaderFloatMode{0xc0u, true, true, false}); - Check(ExpectedIeee, "IEEE=1"); - CheckOmodRefused(*device); + Run(*device, Code, Rows, std::nullopt); + Check(Expected, Names, "no float mode"); + Run(*device, Code, Rows, ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}); + Check(Expected, Names, "IEEE=0"); + Run(*device, Code, Rows, ShaderRecompiler::ShaderFloatMode{0xc0u, true, true, false}); + Check(ExpectedIeee, Names, "IEEE=1"); + Run(*device, OmodCode, OmodRows, std::nullopt); + Check(OmodExpected, OmodNames, "no float mode"); + Run(*device, OmodCode, OmodRows, ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}); + Check(OmodExpected, OmodNames, "IEEE=0 f32 denormals flushed"); + Run(*device, OmodCode, OmodRows, ShaderRecompiler::ShaderFloatMode{0xf0u, true, false, false}); + Check(OmodExpected, OmodNames, "IEEE=0 f32 denormals kept"); + Run(*device, OmodCode, OmodRows, ShaderRecompiler::ShaderFloatMode{0xc0u, true, true, false}); + Check(OmodExpectedIeee, OmodNames, "IEEE=1 f32 denormals flushed"); + Run(*device, OmodCode, OmodRows, ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}); + Check(OmodExpectedIeee, OmodNames, "IEEE=1 f32 denormals kept"); std::puts("vop3 cube clamp tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/shaders/SampleOrderFill.frag b/core/libs/prx/libSceAgcDriver/tests/shaders/SampleOrderFill.frag new file mode 100644 index 0000000000..0f0df0cae8 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/shaders/SampleOrderFill.frag @@ -0,0 +1,22 @@ +#version 450 +#ifdef STENCIL +#extension GL_ARB_shader_stencil_export : require +#endif + +layout(std430, binding = 0) readonly buffer Values { + uvec2 values[]; +}; + +layout(push_constant) uniform Fill { + uint width; + uint count; +} fill; + +void main() { + const uvec2 at = uvec2(gl_FragCoord.xy); + const uint slot = (at.y * fill.width + at.x) * fill.count + uint(gl_SampleID); + gl_FragDepth = uintBitsToFloat(values[slot].x); +#ifdef STENCIL + gl_FragStencilRefARB = int(values[slot].y); +#endif +} diff --git a/core/libs/prx/libSceAgcDriver/tests/shaders/SampleOrderFill.vert b/core/libs/prx/libSceAgcDriver/tests/shaders/SampleOrderFill.vert new file mode 100644 index 0000000000..05606ecec4 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/shaders/SampleOrderFill.vert @@ -0,0 +1,6 @@ +#version 450 + +void main() { + const vec2 corner = vec2(float((gl_VertexIndex << 1) & 2), float(gl_VertexIndex & 2)); + gl_Position = vec4(corner * 2.0 - 1.0, 0.0, 1.0); +} diff --git a/core/libs/prx/libSceAgcDriver/tests/shaders/SampleOrderRead.comp b/core/libs/prx/libSceAgcDriver/tests/shaders/SampleOrderRead.comp new file mode 100644 index 0000000000..89a0fed927 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/shaders/SampleOrderRead.comp @@ -0,0 +1,39 @@ +#version 450 +#extension GL_EXT_samplerless_texture_functions : require + +layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in; + +#ifdef COLOR +layout(binding = 0) uniform texture2DMS colors; +#else +layout(binding = 0) uniform texture2DMS depths; +#ifdef STENCIL +layout(binding = 1) uniform utexture2DMS stencils; +#endif +#endif + +layout(std430, binding = 2) buffer Values { + uvec4 values[]; +}; + +layout(push_constant) uniform Read { + uvec2 extent; + uint count; +} read; + +void main() { + const uvec2 at = gl_GlobalInvocationID.xy; + if (at.x >= read.extent.x || at.y >= read.extent.y) return; + for (uint index = 0u; index < read.count; ++index) { + const uint slot = (at.y * read.extent.x + at.x) * read.count + index; +#ifdef COLOR + values[slot] = floatBitsToUint(texelFetch(colors, ivec2(at), int(index))); +#else + uvec4 value = uvec4(floatBitsToUint(texelFetch(depths, ivec2(at), int(index)).r), 0u, 0u, 0u); +#ifdef STENCIL + value.y = texelFetch(stencils, ivec2(at), int(index)).r; +#endif + values[slot] = value; +#endif + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/shaders/SampleOrderTexels.comp b/core/libs/prx/libSceAgcDriver/tests/shaders/SampleOrderTexels.comp new file mode 100644 index 0000000000..b169405ba9 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/shaders/SampleOrderTexels.comp @@ -0,0 +1,25 @@ +#version 450 + +layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in; + +layout(binding = 0, TEXEL_FORMAT) uniform restrict uimage2DMS texels; + +layout(std430, binding = 1) buffer Values { + uvec4 values[]; +}; + +layout(push_constant) uniform Access { + uvec2 extent; + uint count; + uint store; +} access; + +void main() { + const uvec2 at = gl_GlobalInvocationID.xy; + if (at.x >= access.extent.x || at.y >= access.extent.y) return; + for (uint index = 0u; index < access.count; ++index) { + const uint slot = (at.y * access.extent.x + at.x) * access.count + index; + if (access.store != 0u) imageStore(texels, ivec2(at), int(index), values[slot]); + else values[slot] = imageLoad(texels, ivec2(at), int(index)); + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/test_shader_cache_housekeeping.py b/core/libs/prx/libSceAgcDriver/tests/test_shader_cache_housekeeping.py new file mode 100644 index 0000000000..ac8853b3c1 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/test_shader_cache_housekeeping.py @@ -0,0 +1,100 @@ +import os +import re +import shutil +import subprocess +import sys +import tempfile +import time +from pathlib import Path + + +def native(path): + return Path('\\\\?\\' + str(path)) if os.name == 'nt' else path + + +def snapshot(directory): + return {str(path.relative_to(directory)): None if path.is_dir() else path.read_bytes() + for path in directory.rglob('*')} + + +def main(): + executable = str(Path(sys.argv[1]).resolve()) + mode = sys.argv[2] + assert mode in ('plain', 'plain-long', 'extended', 'extended-long', 'device') + assert os.name == 'nt' or mode in ('plain', 'plain-long') + sandbox = Path(tempfile.mkdtemp(prefix='aps5-housekeeping-')).resolve() + temporary = Path(tempfile.gettempdir()).resolve() + try: + outside = sandbox / 'outside' + outside.mkdir() + environment = dict(os.environ, ANYPS5_NO_SHADER_CACHE='0', + ANYPS5_SHADER_CACHE_DIR=str(sandbox / 'bootstrap')) + + def run(): + return subprocess.run([executable, '--housekeeping-safety'], cwd=outside, + env=environment, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, text=True, errors='replace', timeout=5) + + bootstrap = run() + assert bootstrap.returncode == 0, bootstrap.stdout + match = re.search(r'housekeeping version ([0-9a-f]{16})', bootstrap.stdout) + assert match, bootstrap.stdout + version = match.group(1) + siblings = [digit + version[1:] for digit in '0123' if digit != version[0]] + root = sandbox / 'cache-\u00e4\u6f22' + if mode.endswith('long'): + while len(str(root)) < 230: + root /= 'd' * 40 + root /= 'd' * max(1, 269 - len(str(root))) + assert len(str(root)) >= 270 + root = native(root) + current = root / version + current.mkdir(parents=True) + old = time.time() - 30 * 24 * 3600 + for parent in (root, outside): + for index, name in enumerate(siblings): + folder = parent / name + folder.mkdir() + (folder / 'sentinel').write_bytes(b'preserve outside the cache root') + if index != 1: + (folder / 'last-used').write_bytes(b'') + os.utime(folder / 'last-used', (old if index == 0 else time.time(),) * 2) + os.utime(folder, (old, old)) + (outside / 'outside.tmp.1').write_bytes(b'not a cache temporary') + os.utime(outside / 'outside.tmp.1', (old, old)) + for name in ('shader.bin', 'old.tmp.1', 'fresh.tmp.1', 'last-used'): + (current / name).write_bytes(b'cached bytes') + for name in ('shader.bin', 'old.tmp.1', 'last-used'): + os.utime(current / name, (old, old)) + before = snapshot(outside) + if mode.startswith('extended'): + environment['ANYPS5_SHADER_CACHE_DIR'] = str(root) + elif mode == 'device': + environment['ANYPS5_SHADER_CACHE_DIR'] = '\\\\.\\' + str(root)[4:] + else: + environment['ANYPS5_SHADER_CACHE_DIR'] = str(root)[4:] if os.name == 'nt' else str(root) + started = time.time() + try: + result = run() + except subprocess.TimeoutExpired as error: + assert snapshot(outside) == before, 'housekeeping changed files outside its root' + raise AssertionError('housekeeping did not return within five seconds; child terminated') from error + assert snapshot(outside) == before, 'housekeeping changed files outside its root' + assert result.returncode == 0, result.stdout + assert 'housekeeping version ' + version in result.stdout, result.stdout + assert (current / 'last-used').stat().st_mtime >= started - 2, 'active cache stamp was not refreshed' + skipped = mode.startswith('extended') or mode == 'device' + for name in siblings[:2]: + assert (root / name).exists() == skipped, 'unexpected stale cache directory eviction' + assert (root / siblings[2] / 'sentinel').read_bytes() == b'preserve outside the cache root' + assert (current / 'old.tmp.1').exists() == skipped, 'unexpected old temporary removal' + assert (current / 'fresh.tmp.1').read_bytes() == b'cached bytes' + assert (current / 'shader.bin').read_bytes() == b'cached bytes' + print(mode + ': returned, active stamp refreshed, outside tree intact, eviction policy verified') + finally: + assert sandbox.parent == temporary and sandbox.name.startswith('aps5-housekeeping-') + shutil.rmtree(native(sandbox)) + + +if __name__ == '__main__': + main() diff --git a/core/libs/prx/libSceAgcDriver/tests/test_shader_cache_prebuilt_discovery.py b/core/libs/prx/libSceAgcDriver/tests/test_shader_cache_prebuilt_discovery.py new file mode 100644 index 0000000000..2b7523f8ab --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/test_shader_cache_prebuilt_discovery.py @@ -0,0 +1,74 @@ +import os +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + + +def native(path): + return Path('\\\\?\\' + str(path)) if os.name == 'nt' else path + + +def snapshot(directory): + return {str(path.relative_to(directory)): None if path.is_dir() else path.read_bytes() + for path in directory.rglob('*')} + + +def main(): + executable = str(Path(sys.argv[1]).resolve()) + mode = sys.argv[2] + assert mode in ('plain', 'relative', 'extended', 'extended-forward', 'device', 'plain-long', 'extended-long') + assert os.name == 'nt' or mode in ('plain', 'relative', 'plain-long') + temporary = Path(tempfile.gettempdir()).resolve() + sandbox = Path(tempfile.mkdtemp(prefix='aps5-prebuilt-discovery-')).resolve() + try: + outside = sandbox / 'outside' + outside.mkdir() + (outside / 'z.pack').write_bytes(b'outside directory canary') + (outside / 'sentinel').write_bytes(b'unchanged outside the cache root') + original = snapshot(outside) + for layout in ('pack', 'loose', 'versioned', 'file', 'empty', 'missing'): + root = sandbox / (layout + '-\u00e4\u6f22') + if mode.endswith('long'): + while len(str(root)) < 270: + root /= 'd' * 40 + assert len(str(root / 'prebuilt')) >= 278 + root = native(root) + root.mkdir(parents=True) + plain = str(root)[4:] if os.name == 'nt' else str(root) + alias = plain + if mode.startswith('extended'): + alias = str(root) + if mode == 'extended-forward': + alias = '\\\\?\\' + plain.replace('\\', '/') + elif mode == 'device': + alias = '\\\\.\\' + plain + elif mode == 'relative': + alias = os.path.join('.', os.path.relpath(plain, outside)) + environment = dict(os.environ, ANYPS5_NO_SHADER_CACHE='0', ANYPS5_SHADER_CACHE_DIR=alias, + APS5_NO_PIPELINE_JOURNAL='1') + + def run(action): + try: + result = subprocess.run([executable, '--' + action + '-prebuilt-discovery', layout], cwd=outside, + env=environment, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + text=True, errors='replace', timeout=5) + except subprocess.TimeoutExpired as error: + raise AssertionError('prebuilt discovery did not return within five seconds; child terminated') from error + assert snapshot(outside) == original, 'prebuilt discovery changed files outside its root' + assert result.returncode == 0, mode + ' ' + layout + ':\n' + result.stdout + assert 'prebuilt discovery ' + ('seeded' if action == 'seed' else 'verified') + ' ' + layout in result.stdout + + run('seed') + before = snapshot(root) + run('verify') + assert snapshot(root) == before, 'prebuilt discovery modified its read-only input' + print(mode + ': six prebuilt layouts verified; cache and outside trees unchanged') + finally: + assert sandbox.parent == temporary and sandbox.name.startswith('aps5-prebuilt-discovery-') + shutil.rmtree(native(sandbox)) + + +if __name__ == '__main__': + main() diff --git a/core/libs/prx/libSceAgcDriver/tools/Pm4Replay.cpp b/core/libs/prx/libSceAgcDriver/tools/Pm4Replay.cpp index 7c44c8da28..742fab2a5c 100644 --- a/core/libs/prx/libSceAgcDriver/tools/Pm4Replay.cpp +++ b/core/libs/prx/libSceAgcDriver/tools/Pm4Replay.cpp @@ -1,4 +1,5 @@ #include "Pm4Analysis.hpp" +#include #include #include #include @@ -14,15 +15,15 @@ int usage() { return 2; } -} - -int main(int argc, char** argv) { +template +int run(int argc, TChar** argv) { + constexpr std::string_view Full = "--full"; std::filesystem::path directory; bool full = false; for (int index = 1; index < argc; ++index) { - const std::string_view argument = argv[index]; - if (argument == "--full") full = true; - else if (!argument.starts_with("-") && directory.empty()) directory = argument; + const std::basic_string_view argument = argv[index]; + if (std::ranges::equal(argument, Full)) full = true; + else if (!argument.starts_with(TChar('-')) && directory.empty()) directory = argument; else return usage(); } if (directory.empty()) return usage(); @@ -33,3 +34,15 @@ int main(int argc, char** argv) { return 2; } } + +} + +#ifdef _WIN32 +int wmain(int argc, wchar_t** argv) { + return run(argc, argv); +} +#else +int main(int argc, char** argv) { + return run(argc, argv); +} +#endif diff --git a/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp b/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp index 37c6491ec4..e4eeec9174 100644 --- a/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp +++ b/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp @@ -48,6 +48,7 @@ bool g_materialize = false; bool g_graph = false; bool g_maintenance8 = false; bool g_robust2 = false; +bool g_waveLdsMemoryBarriers = false; bool g_code = false; bool Replay(const char* path) { @@ -147,6 +148,7 @@ bool Replay(const char* path) { try { if (g_maintenance8) request.request.target.nonConstantImageOffsets = true; if (g_robust2) request.request.target.robustBufferAccess2 = true; + if (g_waveLdsMemoryBarriers) request.request.target.waveLdsMemoryBarriers = true; const auto result = ShaderRecompiler::Recompile(request.request); std::printf(" recompiled: %zu SPIR-V words\n", result.spirv.size()); if (g_spirv) { @@ -184,7 +186,7 @@ bool Replay(const char* path) { int main(int argc, char** argv) { if (argc < 2) { - std::fprintf(stderr, "usage: agc_shader_replay [--dis] [--asm] [--cfg] [--mem] [--spv] [--prepare] [--materialize] [--code] [--maintenance8] [--robust2] ...\n the driver writes shader_
.req files when APS5_DUMP_SHADERS is set; shader_
.prepare.req holds a request whose preparation failed before any resource capture, so replay it with --prepare\n"); + std::fprintf(stderr, "usage: agc_shader_replay [--dis] [--asm] [--cfg] [--mem] [--spv] [--prepare] [--materialize] [--code] [--maintenance8] [--robust2] [--wave-lds-memory-barriers] ...\n the driver writes shader_
.req files when APS5_DUMP_SHADERS is set; shader_
.prepare.req holds a request whose preparation failed before any resource capture, so replay it with --prepare\n"); return 2; } int failures = 0; @@ -209,6 +211,10 @@ int main(int argc, char** argv) { g_robust2 = true; continue; } + if (std::string(argv[i]) == "--wave-lds-memory-barriers") { + g_waveLdsMemoryBarriers = true; + continue; + } if (std::string(argv[i]) == "--mem") { g_memory = true; continue; diff --git a/core/libs/prx/libSceAjm.native/CMakeLists.txt b/core/libs/prx/libSceAjm.native/CMakeLists.txt index 94f14ef4bc..51b7c2507f 100644 --- a/core/libs/prx/libSceAjm.native/CMakeLists.txt +++ b/core/libs/prx/libSceAjm.native/CMakeLists.txt @@ -22,6 +22,13 @@ add_library(atrac9 STATIC EXCLUDE_FROM_ALL target_include_directories(atrac9 PRIVATE ${LIBATRAC9_DIR}) set_target_properties(atrac9 PROPERTIES POSITION_INDEPENDENT_CODE ON) +set(VGMSTREAM_ATRAC9_DIR ${CMAKE_SOURCE_DIR}/3rdparty/vgmstream/src/coding/libs) +add_library(atrac9_vibration STATIC EXCLUDE_FROM_ALL + ${VGMSTREAM_ATRAC9_DIR}/atrac9_dec.c + ${VGMSTREAM_ATRAC9_DIR}/atrac9_internal.c +) +set_target_properties(atrac9_vibration PROPERTIES POSITION_INDEPENDENT_CODE ON C_VISIBILITY_PRESET hidden) + add_library(libSceAjm.native SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Ajm.cpp @@ -30,8 +37,9 @@ add_library(libSceAjm.native SHARED EXCLUDE_FROM_ALL target_include_directories(libSceAjm.native PRIVATE ${LIBS_INCLUDE_DIR} ${LIBATRAC9_DIR} + ${VGMSTREAM_ATRAC9_DIR} ) -target_link_libraries(libSceAjm.native PRIVATE atrac9 ffmpeg libc libkernel) +target_link_libraries(libSceAjm.native PRIVATE atrac9 atrac9_vibration ffmpeg libc libkernel) configure_windows_unwind(libSceAjm.native) if(BUILD_TESTING) @@ -58,6 +66,11 @@ if(BUILD_TESTING) target_link_libraries(guest_ajm_tests PRIVATE libSceAjm.native libc) configure_windows_unwind(guest_ajm_tests) add_test(NAME guest_ajm COMMAND guest_ajm_tests) + add_test_executable(guest_ajm_vibration_tests ${LIBS_INCLUDE_DIR}/tests/GuestAjmVibration.cpp) + target_include_directories(guest_ajm_vibration_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_ajm_vibration_tests PRIVATE libSceAjm.native libc) + configure_windows_unwind(guest_ajm_vibration_tests) + add_test(NAME guest_ajm_vibration COMMAND guest_ajm_vibration_tests) if(UNIX) add_test_executable(guest_ajm_teardown_tests ${LIBS_INCLUDE_DIR}/tests/GuestAjmTeardown.cpp) target_include_directories(guest_ajm_teardown_tests PRIVATE ${LIBS_INCLUDE_DIR}) @@ -66,7 +79,7 @@ if(BUILD_TESTING) add_test(NAME guest_ajm_teardown COMMAND guest_ajm_teardown_tests $) endif() if(WIN32) - set_tests_properties(guest_ajm PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_ajm guest_ajm_vibration PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceAjm.native/Export.cpp b/core/libs/prx/libSceAjm.native/Export.cpp index e6c8fcbd75..2df65a236d 100644 --- a/core/libs/prx/libSceAjm.native/Export.cpp +++ b/core/libs/prx/libSceAjm.native/Export.cpp @@ -42,4 +42,28 @@ 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) { + return instance_id >> 14; +} + +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..69fc72c729 100644 --- a/core/libs/prx/libSceAjm.native/src/Ajm.cpp +++ b/core/libs/prx/libSceAjm.native/src/Ajm.cpp @@ -4,12 +4,14 @@ #include "prx/libc/include/General.hpp" #include "libatrac9.h" extern "C" { +#include "atrac9_dec.h" #include #include #include #include } #include +#include #include #include #include @@ -18,7 +20,6 @@ extern "C" { #include #include #include -#include #include #include #include @@ -135,6 +136,7 @@ struct Instance { std::uint32_t codec = 0; std::uint64_t flags = 0; void* decoder = nullptr; + atrac9_handle_t* vibrationDecoder = nullptr; std::vector channelDecoders; std::vector channelBytes; std::vector blockPcm; @@ -174,6 +176,7 @@ struct Instance { ~Instance() { if (decoder) Atrac9ReleaseHandle(decoder); + atrac9_free(vibrationDecoder); for (void* channelDecoder : channelDecoders) Atrac9ReleaseHandle(channelDecoder); if (mp3) avcodec_free_context(&mp3); if (opus) avcodec_free_context(&opus); @@ -187,7 +190,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, @@ -302,7 +305,23 @@ bool ParseMultichannelConfig(const std::uint8_t* config, At9MultichannelConfig& return true; } +bool IsAt9Vibration(const std::uint8_t* config) { + return config[0] == 0xFE && ((config[1] >> 1u) & 7u) >= 6u; +} + bool DescribeAt9Config(const std::uint8_t* configData, Atrac9CodecInfo& info) { + if (IsAt9Vibration(configData)) { + atrac9_info_t vibration{}; + if (atrac9_get_config_info(configData, &vibration) != 0 || vibration.sample_rate <= 0) return false; + info.channels = vibration.channels; + info.channelConfigIndex = (configData[1] >> 1u) & 7u; + info.samplingRate = vibration.sample_rate; + info.superframeSize = vibration.superframe_size; + info.framesInSuperframe = vibration.frames_per_superframe; + info.frameSamples = vibration.frame_samples; + std::memcpy(info.configData, configData, ATRAC9_CONFIG_DATA_SIZE); + return true; + } At9MultichannelConfig multichannel{}; const bool isMultichannel = ParseMultichannelConfig(configData, multichannel); unsigned char config[ATRAC9_CONFIG_DATA_SIZE]; @@ -325,12 +344,18 @@ bool DescribeAt9Config(const std::uint8_t* configData, Atrac9CodecInfo& info) { bool ResetDecoder(Instance& instance) { if (instance.decoder) Atrac9ReleaseHandle(instance.decoder); instance.decoder = nullptr; + atrac9_free(instance.vibrationDecoder); + instance.vibrationDecoder = nullptr; for (void* channelDecoder : instance.channelDecoders) Atrac9ReleaseHandle(channelDecoder); instance.channelDecoders.clear(); instance.superframeRemaining = 0; instance.frameInSuperframe = 0; unsigned char config[ATRAC9_CONFIG_DATA_SIZE]; std::memcpy(config, instance.info.configData, sizeof(config)); + if (IsAt9Vibration(config)) { + instance.vibrationDecoder = atrac9_init(config); + return instance.vibrationDecoder != nullptr; + } At9MultichannelConfig multichannel{}; if (!ParseMultichannelConfig(config, multichannel)) { instance.decoder = Atrac9GetHandle(); @@ -345,7 +370,7 @@ bool ResetDecoder(Instance& instance) { } bool HasAt9Decoder(const Instance& instance) { - return instance.decoder || !instance.channelDecoders.empty(); + return instance.decoder || instance.vibrationDecoder || !instance.channelDecoders.empty(); } int DecodeAt9Block(void* decoder, std::uint32_t encoding, const std::uint8_t* data, int size, std::uint8_t* pcm, int* used) { @@ -359,6 +384,25 @@ int DecodeAt9Block(void* decoder, std::uint32_t encoding, const std::uint8_t* da constexpr int AT9_MULTICHANNEL_BLOCK_OVERRUN = 0x7F000001; int DecodeAt9Frame(Instance& instance, const std::uint8_t* data, int size, std::uint8_t* pcm, std::uint32_t encoding, std::size_t sampleBytes, int* used) { + if (instance.vibrationDecoder) { + if (encoding > 2) NotImplemented_nid_no_patch("AJM ATRAC9 vibration PCM encoding other than PCM16, PCM32 and float"); + int samples = 0; + if (encoding == 0) samples = atrac9_decode_pcm16(instance.vibrationDecoder, data, size, reinterpret_cast(pcm), used); + else if (encoding == 2) samples = atrac9_decode_float(instance.vibrationDecoder, data, size, reinterpret_cast(pcm), used); + else { + std::array decoded{}; + samples = atrac9_decode(instance.vibrationDecoder, data, size, decoded.data(), used); + if (samples > 0) { + const auto count = static_cast(samples) * instance.info.channels; + if (count > decoded.size()) return AT9_MULTICHANNEL_BLOCK_OVERRUN; + for (std::size_t index = 0; index < count; ++index) { + const auto value = static_cast(std::clamp(static_cast(decoded[index]) * 65536.0, -2147483648.0, 2147483647.0)); + std::memcpy(pcm + index * sizeof(value), &value, sizeof(value)); + } + } + } + return samples < 0 ? samples : samples == instance.info.frameSamples ? 0 : AT9_MULTICHANNEL_BLOCK_OVERRUN; + } if (instance.channelDecoders.empty()) return DecodeAt9Block(instance.decoder, encoding, data, size, pcm, used); const std::size_t channels = instance.channelDecoders.size(); const auto frameSamples = static_cast(instance.info.frameSamples); @@ -1833,7 +1877,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 +1907,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 +1929,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/libSceAjmi/Export.cpp b/core/libs/prx/libSceAjmi/Export.cpp index fb34206c21..83e8ac5ffa 100644 --- a/core/libs/prx/libSceAjmi/Export.cpp +++ b/core/libs/prx/libSceAjmi/Export.cpp @@ -7,4 +7,14 @@ extern "C" { APS5_DUMMY_FUN +int APS5_VABI sceAjmBatchJobInlineBuffer(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAjmBatchJobRunBufferRa(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceAmpr/Export.cpp b/core/libs/prx/libSceAmpr/Export.cpp index fd0d7cf527..6e55ecbd7b 100644 --- a/core/libs/prx/libSceAmpr/Export.cpp +++ b/core/libs/prx/libSceAmpr/Export.cpp @@ -7,7 +7,6 @@ #include "prx/libkernel/Apr/include/AprCommandBuffer.hpp" #include "prx/libkernel/Time/include/Time.hpp" #include -#include #include #include #include @@ -226,18 +225,6 @@ static int RecordWriteAddressFromCounterPair(Apr::CommandBufferObject* buffer, v return AppendCommand(buffer, Apr::Opcode::WriteAddressFromCounterPair, Apr::WriteAddressFromCounterCommand{{}, reinterpret_cast(address), counter, counter + 1u}); } -static constexpr int ThrdSuccess = 0; -static constexpr int ThrdNomem = 1; -static constexpr int ThrdTimedout = 2; -static constexpr int ThrdBusy = 3; -static constexpr int ThrdError = 4; - -static constexpr int CppDeviceOrResourceBusy = 0; -static constexpr int CppInvalidArgument = 1; -static constexpr int CppResourceUnavailableTryAgain = 6; - -static constexpr int MtxPlain = 0x1; - static std::atomic_flag sharedPtrSpinLock; struct AmmUsageStatsData { @@ -252,55 +239,10 @@ static_assert(sizeof(AmmUsageStatsData) == 0x18); static constexpr std::uint16_t AmmPageTablePoolEntries = 512; static constexpr std::uint32_t AmmRingIdleFlags = 0x7; -struct GuestPad { - unsigned (APS5_VABI* const* vtable)(GuestPad*); - void* condition; - void* mutex; - bool started; -}; - -static_assert(offsetof(GuestPad, condition) == 0x8 && offsetof(GuestPad, mutex) == 0x10 && offsetof(GuestPad, started) == 0x18 && sizeof(GuestPad) == 0x20); - extern "C" { -int APS5_VABI pthread_create_nid_postfix(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg); -int APS5_VABI pthread_join_nid_postfix(Pthread thread, void** value); -int APS5_VABI _Mtx_init_nid_postfix(void** handle, int type); -void APS5_VABI _Mtx_destroy_nid_postfix(void** handle); -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_signal_nid_postfix(void** handle); -int APS5_VABI wcsrtombs_s_nid_postfix(std::size_t* result, char* destination, std::size_t capacity, const std::uint16_t** source, std::size_t limit, void* state); +int APS5_VABI wcstombs_s_nid_postfix(std::size_t* result, char* destination, std::size_t capacity, const std::uint16_t* source, std::size_t limit); [[noreturn]] void APS5_VABI __cxa_call_terminate_nid_postfix(void* exception); -int APS5_VABI _Cnd_init_nid_postfix(void** handle); -int SignalCondition_nid_no_patch(void** handle); -[[noreturn]] void ThrowCppError_nid_no_patch(int code); -} - -static void RequireThrd(int result, const char* what) { - switch (result) { - case ThrdSuccess: - return; - case ThrdNomem: - case ThrdTimedout: - ThrowCppError_nid_no_patch(CppResourceUnavailableTryAgain); - case ThrdBusy: - ThrowCppError_nid_no_patch(CppDeviceOrResourceBusy); - case ThrdError: - ThrowCppError_nid_no_patch(CppInvalidArgument); - } - throw std::runtime_error(std::string(what) + " failed with " + std::to_string(result)); -} - -static void* APS5_VABI PadEntry_nid_no_patch(void* argument) { - auto* pad = static_cast(argument); - const auto go = pad->vtable[0]; - return reinterpret_cast(static_cast(go(pad))); -} - -extern "C" { int APS5_VABI sceAmprCommandBufferWriteAddressOnCompletion(Apr::CommandBufferObject* buffer, volatile std::uint64_t* address, std::uint64_t value) { if (!ValidWriteAddress(address)) return SCE_KERNEL_ERROR_EINVAL; @@ -347,6 +289,7 @@ std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWaitOnCounter() { return sizeof std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWriteKernelEventQueueOnCompletion() { return sizeof(Apr::WriteKernelEventQueueCommand); } int APS5_VABI sceAmprAprCommandBufferConstructor(Apr::CommandBufferObject* buffer, uint64_t* gatherState, uint64_t* scatterState) { + if (!buffer || !gatherState || !scatterState) return SCE_KERNEL_ERROR_EINVAL; buffer->type = Apr::BufferType::Apr; *gatherState = 0; *scatterState = 0; @@ -452,6 +395,7 @@ int APS5_VABI sceAmprCommandBufferConstructNop(Apr::CommandBufferObject* buffer, } int APS5_VABI sceAmprCommandBufferConstructor(Apr::CommandBufferObject* buffer) { + if (!buffer) return SCE_KERNEL_ERROR_EINVAL; *buffer = {Apr::BufferType::Generic, 0, 0, 0, 0, nullptr}; return 0; } @@ -506,12 +450,14 @@ int APS5_VABI sceAmprCommandBufferPushMarkerWithColor(Apr::CommandBufferObject* } int APS5_VABI sceAmprCommandBufferReset(Apr::CommandBufferObject* buffer) { + if (!buffer) return SCE_KERNEL_ERROR_EINVAL; buffer->offset = 0; buffer->numCommands = 0; return 0; } int APS5_VABI sceAmprCommandBufferSetBuffer(Apr::CommandBufferObject* buffer, void* memory, uint32_t size) { + if (!buffer) return SCE_KERNEL_ERROR_EINVAL; buffer->base = static_cast(memory); buffer->size = size; buffer->offset = 0; @@ -901,59 +847,6 @@ int APS5_VABI sceAmprAmmWaitCommandBufferCompletion(std::uint32_t id) { return AmmSubmitted_nid_no_patch(id) ? 0 : SCE_KERNEL_ERROR_ESRCH; } -[[noreturn]] void APS5_VABI _ZSt16_Throw_Cpp_errori_nid_postfix(int code) { - ThrowCppError_nid_no_patch(code); -} -int APS5_VABI _Thrd_join_nid_postfix(Pthread thread, int* code) { - void* result = nullptr; - if (pthread_join_nid_postfix(thread, &result) != 0) return ThrdError; - if (code) *code = static_cast(reinterpret_cast(result)); - return ThrdSuccess; -} - -void APS5_VABI _ZNSt4_Pad8_ReleaseEv_nid_postfix(GuestPad* pad) { - if (!pad) APS5_INVALID_ARG_EX; - RequireThrd(_Mtx_lock_nid_postfix(&pad->mutex), "_Pad::_Release: _Mtx_lock"); - pad->started = true; - RequireThrd(_Cnd_signal_nid_postfix(&pad->condition), "_Pad::_Release: _Cnd_signal"); - RequireThrd(_Mtx_unlock_nid_postfix(&pad->mutex), "_Pad::_Release: _Mtx_unlock"); -} - -void APS5_VABI _ZNSt4_PadC2Ev_nid_postfix(GuestPad* pad) { - if (!pad) APS5_INVALID_ARG_EX; - RequireThrd(_Cnd_init_nid_postfix(&pad->condition), "_Pad::_Pad: _Cnd_init"); - const int created = _Mtx_init_nid_postfix(&pad->mutex, MtxPlain); - if (created != ThrdSuccess) { - _Cnd_destroy_nid_postfix(&pad->condition); - RequireThrd(created, "_Pad::_Pad: _Mtx_init"); - } - pad->started = false; - const int locked = _Mtx_lock_nid_postfix(&pad->mutex); - if (locked != ThrdSuccess) { - _Mtx_destroy_nid_postfix(&pad->mutex); - _Cnd_destroy_nid_postfix(&pad->condition); - RequireThrd(locked, "_Pad::_Pad: _Mtx_lock"); - } -} - -void APS5_VABI _ZNSt4_PadD2Ev_nid_postfix(GuestPad* pad) { - if (!pad) APS5_INVALID_ARG_EX; - RequireThrd(_Mtx_unlock_nid_postfix(&pad->mutex), "_Pad::~_Pad: _Mtx_unlock"); - _Mtx_destroy_nid_postfix(&pad->mutex); - _Cnd_destroy_nid_postfix(&pad->condition); -} - -int APS5_VABI _Thrd_id_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -void APS5_VABI _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(GuestPad* pad, Pthread* thread) { - if (!pad || !thread) APS5_INVALID_ARG_EX; - const int created = pthread_create_nid_postfix(thread, nullptr, PadEntry_nid_no_patch, pad); - if (created != 0) ThrowCppError_nid_no_patch(CppResourceUnavailableTryAgain); - while (!pad->started) RequireThrd(_Cnd_wait_nid_postfix(&pad->condition, &pad->mutex), "_Pad::_Launch: _Cnd_wait"); -} int APS5_VABI _ZNSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -992,11 +885,6 @@ void APS5_VABI _Lock_shared_ptr_spin_lock_nid_postfix(void) { } -int APS5_VABI wcstombs_s_nid_postfix(std::size_t* result, char* destination, std::size_t capacity, const std::uint16_t* source, std::size_t limit) { - std::array state{}; - return wcsrtombs_s_nid_postfix(result, destination, capacity, &source, limit, state.data()); -} - [[noreturn]] void APS5_VABI __cxa_call_unexpected_nid_postfix(void* unwindException) { __cxa_call_terminate_nid_postfix(unwindException); } @@ -1046,6 +934,11 @@ void APS5_VABI sceAmprIgnoreHandlerS(const char* message, void* pointer, int err SonyIgnoreHandlerS(message, pointer, error); } +APS5_EXPORT("sZLrjx-yEx4", sceAmprWcstombsS); +int APS5_VABI sceAmprWcstombsS(std::size_t* result, char* destination, std::size_t capacity, const std::uint16_t* source, std::size_t limit) { + return wcstombs_s_nid_postfix(result, destination, capacity, source, limit); +} + APS5_EXPORT("VUzjXknPPBs", sceAmprMbstowcs); std::size_t APS5_VABI sceAmprMbstowcs(std::uint16_t* dest, const char* src, std::size_t size) { return SonyMbstowcs(dest, src, size); 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 be7cad24d9..bb0eba21ea 100644 --- a/core/libs/prx/libSceAppContent/Export.cpp +++ b/core/libs/prx/libSceAppContent/Export.cpp @@ -2,6 +2,7 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/RemoveTree.hpp" #include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" #include #include @@ -49,7 +50,7 @@ bool IsTemporaryMountPoint(const AppContentMountPoint* mountPoint) { } static void ClearDirectory(const std::filesystem::path& directory) { - for (const auto& entry : std::filesystem::directory_iterator(directory)) std::filesystem::remove_all(entry.path()); + for (const auto& entry : std::filesystem::directory_iterator(directory)) RemoveTree(entry.path()); RecordWrittenPath_nid_no_patch(directory); } @@ -95,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); @@ -103,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; @@ -189,4 +190,89 @@ int APS5_VABI sceAppContentGetAddcontDownloadProgress(uint32_t service_label, co if (!entitlement_label || !progress) return SCE_APP_CONTENT_ERROR_PARAMETER; return SCE_APP_CONTENT_ERROR_NOT_FOUND; } +int APS5_VABI sceAppContentAddcontEnqueueDownloadByEntitlemetId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppContentAddcontEnqueueDownloadSp(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppContentAddcontMountByEntitlemetId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppContentAddcontShrink(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppContentAppParamGetString(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppContentDownload0Expand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppContentDownload0Shrink(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppContentDownload1Expand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppContentDownload1Shrink(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppContentGetAddcontInfoByEntitlementId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppContentGetAddcontInfoListByIroTag(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppContentGetDownloadedStoreCountry(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppContentGetEntitlementKey(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppContentGetPftFlag(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppContentGetRegion(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppContentRequestPatchInstall(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAppContentTemporaryDataMount(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceAt9Enc/CMakeLists.txt b/core/libs/prx/libSceAt9Enc/CMakeLists.txt new file mode 100644 index 0000000000..89880a7b92 --- /dev/null +++ b/core/libs/prx/libSceAt9Enc/CMakeLists.txt @@ -0,0 +1,15 @@ +add_library(libSceAt9Enc SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceAt9Enc PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceAt9Enc) + +if(BUILD_TESTING) + add_test_executable(guest_at9_enc_tests ${LIBS_INCLUDE_DIR}/tests/GuestAt9Enc.cpp) + target_include_directories(guest_at9_enc_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_at9_enc_tests PRIVATE libSceAt9Enc libc) + configure_windows_unwind(guest_at9_enc_tests) + add_test(NAME guest_at9_enc COMMAND guest_at9_enc_tests) + if(WIN32) + set_tests_properties(guest_at9_enc PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceAt9Enc/Export.cpp b/core/libs/prx/libSceAt9Enc/Export.cpp new file mode 100644 index 0000000000..77fee077ec --- /dev/null +++ b/core/libs/prx/libSceAt9Enc/Export.cpp @@ -0,0 +1,34 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + + +extern "C" { + +int32_t APS5_VABI sceAt9EncClearContext(void *pAt9EncWorkArea, int32_t *pInternalResult) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int32_t APS5_VABI sceAt9EncCreateEncoder(void *pAt9EncWorkArea, void *pAt9EncParam, int32_t memSize, int32_t *pInternalResult) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int32_t APS5_VABI sceAt9EncEncode(void *pAt9EncWorkArea, const void *pInputBuf, void *pOutputBuf, uint32_t inputBufSize, uint32_t outputBufSize, uint32_t *pOutputDataByte, uint32_t *pUsedBufSize, int32_t *pInternalResult) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int32_t APS5_VABI sceAt9EncFlush(void *pAt9EncWorkArea, void *pOutputBuf, uint32_t outputBufSize, uint32_t *pOutputDataByte, int32_t *pInternalResult) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int32_t APS5_VABI sceAt9EncQueryMemSize(void *pAt9EncParam, int32_t *pMemSize, int32_t *pInternalResult) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceAudio3d/CMakeLists.txt b/core/libs/prx/libSceAudio3d/CMakeLists.txt index 1ed8b159bb..791d2fac7e 100644 --- a/core/libs/prx/libSceAudio3d/CMakeLists.txt +++ b/core/libs/prx/libSceAudio3d/CMakeLists.txt @@ -1,5 +1,6 @@ add_library(libSceAudio3d SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceAudio3d PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceAudio3d PRIVATE libSceAudioOut) configure_windows_unwind(libSceAudio3d) if(BUILD_TESTING) @@ -21,4 +22,14 @@ if(BUILD_TESTING) set_tests_properties(guest_audio3d_port PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_audio3d_mixer_tests ${LIBS_INCLUDE_DIR}/tests/GuestAudio3dMixer.cpp) + target_include_directories(guest_audio3d_mixer_tests PRIVATE + ${LIBS_INCLUDE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_audio3d_mixer_tests PRIVATE libc) + configure_windows_unwind(guest_audio3d_mixer_tests) + add_test(NAME guest_audio3d_mixer COMMAND guest_audio3d_mixer_tests) + if(WIN32) + set_tests_properties(guest_audio3d_mixer PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/prx/libSceAudio3d/Export.cpp b/core/libs/prx/libSceAudio3d/Export.cpp index 47489f9d5e..28854734cc 100644 --- a/core/libs/prx/libSceAudio3d/Export.cpp +++ b/core/libs/prx/libSceAudio3d/Export.cpp @@ -1,23 +1,31 @@ #include -#include #include #include #include #include #include +#include #include #include #include #include +#include +#include "Mixer.hpp" #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" -#include "prx/libc/include/PreciseWait.hpp" + +extern "C" { +int APS5_VABI sceAudioOutOpen(int user_id, int type, int index, std::uint32_t len, std::uint32_t freq, std::uint32_t param); +int APS5_VABI sceAudioOutOutput(int handle, const void* ptr); +int APS5_VABI sceAudioOutClose(int handle); +} namespace { constexpr int AUDIO3D_ERROR_INVALID_PORT = static_cast(0x80EA0002); constexpr int AUDIO3D_ERROR_INVALID_OBJECT = static_cast(0x80EA0003); constexpr int AUDIO3D_ERROR_INVALID_PARAMETER = static_cast(0x80EA0004); +constexpr int AUDIO3D_ERROR_NOT_SUPPORTED = static_cast(0x80EA0008); constexpr int AUDIO3D_ERROR_OUT_OF_RESOURCES = static_cast(0x80EA0006); constexpr int AUDIO3D_ERROR_NOT_READY = static_cast(0x80EA0007); @@ -27,24 +35,44 @@ constexpr std::uint32_t AUDIO3D_OBJECT_INVALID = 0xFFFFFFFF; constexpr std::uint32_t AUDIO3D_RATE_48000 = 0; constexpr std::uint32_t AUDIO3D_SAMPLE_RATE = 48000; constexpr std::uint32_t AUDIO3D_BUFFER_ADVANCE_AND_PUSH = 2; +constexpr int AUDIO3D_AUDIO_OUT_PORT_TYPE = 126; +constexpr std::uint32_t AUDIO3D_AUDIO_OUT_FORMAT_STEREO = 1; constexpr std::uint32_t AUDIO3D_BLOCKING_ASYNC = 0; constexpr std::uint32_t AUDIO3D_BLOCKING_SYNC = 1; constexpr std::uint32_t AUDIO3D_ATTRIBUTE_RESET_STATE = 0x20000; constexpr std::uint32_t AUDIO3D_ATTRIBUTE_LATE_REVERB_LEVEL = 0x10001; constexpr std::uint32_t AUDIO3D_ATTRIBUTE_DOWNMIX_SPREAD_RADIUS = 0x10002; constexpr std::uint32_t AUDIO3D_ATTRIBUTE_DOWNMIX_SPREAD_HEIGHT_AWARE = 0x10003; +constexpr std::uint32_t AUDIO3D_FORMAT_S16 = 0; +constexpr std::uint32_t AUDIO3D_FORMAT_FLOAT = 1; +constexpr std::uint32_t AUDIO3D_OUTPUT_ROUTE_BOTH = 0; +constexpr std::uint32_t AUDIO3D_OUTPUT_ROUTE_HMU_ONLY = 1; +constexpr std::uint32_t AUDIO3D_OUTPUT_ROUTE_TV_ONLY = 2; -using Clock = std::chrono::steady_clock; +struct Audio3dAudioOut { + std::size_t buffer_bytes = 0; + std::deque> pending; +}; + +struct Audio3dBed { + std::vector pcm; + std::uint32_t channels = 0; + std::uint32_t format = 0; + std::uint32_t samples = 0; +}; struct Audio3dPort { bool open = false; std::uint32_t queue_depth = 0; - Clock::duration frame_duration{}; - std::uint32_t advanced = 0; - std::deque playing; + std::uint32_t granularity = 0; + std::uint32_t buffer_mode = 0; std::uint32_t max_objects = 0; std::uint32_t next_object_id = 0; std::set objects; + std::deque beds; + std::deque> mixed; + int audio_out_handle = 0; + std::map audio_out_ports; }; std::mutex g_mutex; @@ -59,9 +87,39 @@ bool PortIsOpen(std::uint32_t port_id) { return port_id == AUDIO3D_PORT_ID && g_port.open; } -std::uint32_t QueueLevel(Clock::time_point now) { - while (!g_port.playing.empty() && g_port.playing.front() <= now) g_port.playing.pop_front(); - return g_port.advanced + static_cast(g_port.playing.size()); +std::uint32_t QueueLevel() { + return static_cast(g_port.mixed.size()); +} + +std::size_t AudioOutBufferBytes(std::uint32_t len, std::uint32_t param) { + static constexpr std::uint32_t channels[] = {1, 2, 8, 1, 2, 8, 8, 8}; + static constexpr std::uint32_t sample_size[] = {2, 2, 2, 4, 4, 4, 2, 4}; + const std::uint32_t format = param & 0xFF; + if (format >= std::size(channels)) return 0; + return static_cast(len) * channels[format] * sample_size[format]; +} + +int DrainAssociatedAudioOuts() { + while (true) { + int handle = 0; + std::vector frame; + { + std::lock_guard lock(g_mutex); + const auto entry = std::find_if(g_port.audio_out_ports.begin(), g_port.audio_out_ports.end(), + [](const auto& candidate) { return !candidate.second.pending.empty(); }); + if (entry == g_port.audio_out_ports.end()) return 0; + handle = entry->first; + frame = std::move(entry->second.pending.front()); + entry->second.pending.pop_front(); + } + const int result = sceAudioOutOutput(handle, frame.data()); + if (result < 0) { + std::lock_guard lock(g_mutex); + const auto entry = g_port.audio_out_ports.find(handle); + if (entry != g_port.audio_out_ports.end()) entry->second.pending.push_front(std::move(frame)); + return result; + } + } } } @@ -72,6 +130,7 @@ void APS5_VABI sceAudio3dGetDefaultOpenParameters(Audio3dOpenParameters* p) { if (p == nullptr) APS5_INVALID_ARG_EX; constexpr Audio3dOpenParameters defaults{0x20, 256, 0, 512, 2, 2, 0, 0}; static_assert(offsetof(Audio3dOpenParameters, num_beds) == 0x20); + static_assert(sizeof(Audio3dOpenParameters) == 0x28); std::memcpy(p, &defaults, defaults.size_this); } @@ -93,15 +152,40 @@ int APS5_VABI sceAudio3dTerminate() { int APS5_VABI sceAudio3dPortAdvance(uint32_t port_id) { std::lock_guard lock(g_mutex); if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; - if (QueueLevel(Clock::now()) >= g_port.queue_depth) Unsupported(__func__, "queue is full"); - ++g_port.advanced; + if (g_port.buffer_mode != AUDIO3D_BUFFER_ADVANCE_AND_PUSH) return AUDIO3D_ERROR_NOT_SUPPORTED; + if (QueueLevel() >= g_port.queue_depth) return AUDIO3D_ERROR_NOT_READY; + std::vector frame; + if (g_port.beds.empty()) { + Audio3dInternal::MixFrame(nullptr, 0, g_port.granularity, frame); + } else { + const Audio3dBed& bed = g_port.beds.front(); + const Audio3dInternal::Source source{ + bed.pcm.data(), + bed.samples, + bed.channels, + bed.format == AUDIO3D_FORMAT_FLOAT, + 1.0f, + 0.0f, + }; + Audio3dInternal::MixFrame(&source, 1, g_port.granularity, frame); + g_port.beds.pop_front(); + } + g_port.mixed.push_back(std::move(frame)); return 0; } int APS5_VABI sceAudio3dPortClose(uint32_t port_id) { - std::lock_guard lock(g_mutex); - if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; - g_port = Audio3dPort{}; + int handle = 0; + std::vector associated; + { + std::lock_guard lock(g_mutex); + if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; + handle = g_port.audio_out_handle; + for (const auto& entry : g_port.audio_out_ports) associated.push_back(entry.first); + g_port = Audio3dPort{}; + } + for (const int port : associated) sceAudioOutClose(port); + if (handle != 0) sceAudioOutClose(handle); return 0; } @@ -109,7 +193,7 @@ int APS5_VABI sceAudio3dPortGetQueueLevel(uint32_t port_id, uint32_t* queue_leve std::lock_guard lock(g_mutex); if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; if (queue_level == nullptr && queue_available == nullptr) return AUDIO3D_ERROR_INVALID_PARAMETER; - const std::uint32_t level = QueueLevel(Clock::now()); + const std::uint32_t level = QueueLevel(); if (queue_level != nullptr) *queue_level = level; if (queue_available != nullptr) *queue_available = g_port.queue_depth - level; return 0; @@ -164,33 +248,53 @@ int APS5_VABI sceAudio3dPortOpen(int user_id, const Audio3dOpenParameters* param g_port = Audio3dPort{}; g_port.open = true; g_port.queue_depth = parameters->queue_depth; + g_port.granularity = parameters->granularity; + g_port.buffer_mode = parameters->buffer_mode; g_port.max_objects = parameters->max_objects; - g_port.frame_duration = std::chrono::duration_cast( - std::chrono::nanoseconds(std::uint64_t{parameters->granularity} * 1000000000u / AUDIO3D_SAMPLE_RATE)); *id = AUDIO3D_PORT_ID; return 0; } int APS5_VABI sceAudio3dPortPush(uint32_t port_id, uint32_t blocking) { - Clock::time_point wait_until; + int handle = 0; + std::uint32_t granularity = 0; { std::lock_guard lock(g_mutex); if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; if (blocking != AUDIO3D_BLOCKING_ASYNC && blocking != AUDIO3D_BLOCKING_SYNC) { Unsupported(__func__, "blocking mode " + std::to_string(blocking)); } - const Clock::time_point now = Clock::now(); - const std::uint32_t level = QueueLevel(now); - Clock::time_point end = g_port.playing.empty() ? now : g_port.playing.back(); - for (; g_port.advanced != 0; --g_port.advanced) { - end += g_port.frame_duration; - g_port.playing.push_back(end); - } - if (blocking == AUDIO3D_BLOCKING_ASYNC || level < g_port.queue_depth) return 0; - wait_until = g_port.playing[level - g_port.queue_depth]; + handle = g_port.audio_out_handle; + granularity = g_port.granularity; } - PreciseSleepUntil(wait_until); - return 0; + if (handle == 0) { + const int opened = sceAudioOutOpen(AUDIO3D_USER_ID_SYSTEM, AUDIO3D_AUDIO_OUT_PORT_TYPE, 0, + granularity, AUDIO3D_SAMPLE_RATE, AUDIO3D_AUDIO_OUT_FORMAT_STEREO); + if (opened < 0) return AUDIO3D_ERROR_OUT_OF_RESOURCES; + std::lock_guard lock(g_mutex); + if (!PortIsOpen(port_id)) { + sceAudioOutClose(opened); + return AUDIO3D_ERROR_INVALID_PORT; + } + if (g_port.audio_out_handle == 0) g_port.audio_out_handle = opened; + else sceAudioOutClose(opened); + } + std::deque> frames; + { + std::lock_guard lock(g_mutex); + if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; + handle = g_port.audio_out_handle; + const std::size_t target = blocking == AUDIO3D_BLOCKING_SYNC ? 0 : g_port.queue_depth - 1; + while (g_port.mixed.size() > target) { + frames.push_back(std::move(g_port.mixed.front())); + g_port.mixed.pop_front(); + } + } + for (const std::vector& frame : frames) { + const int result = sceAudioOutOutput(handle, frame.data()); + if (result < 0) return result; + } + return DrainAssociatedAudioOuts(); } int APS5_VABI sceAudio3dPortSetAttribute(uint32_t port_id, uint32_t attribute_id, const void* attribute, size_t attribute_size) { @@ -242,10 +346,159 @@ int APS5_VABI sceAudio3dObjectSetAttributes(uint32_t port_id, uint32_t object_id } int APS5_VABI sceAudio3dPortFlush(uint32_t port_id) { + int handle = 0; + std::vector associated; + { + std::lock_guard lock(g_mutex); + if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; + g_port.mixed.clear(); + handle = g_port.audio_out_handle; + for (auto& entry : g_port.audio_out_ports) { + entry.second.pending.clear(); + associated.push_back(entry.first); + } + } + for (const int port : associated) sceAudioOutOutput(port, nullptr); + if (handle != 0) sceAudioOutOutput(handle, nullptr); + return 0; +} + +int APS5_VABI sceAudio3dBedWrite2(uint32_t port_id, uint32_t num_channels, uint32_t format, const void* buffer, uint32_t num_samples, uint32_t output_route, bool) { std::lock_guard lock(g_mutex); if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; - g_port.advanced = 0; - g_port.playing.clear(); + if (num_channels != 2 && num_channels != 6 && num_channels != 8) return AUDIO3D_ERROR_INVALID_PARAMETER; + if (format > AUDIO3D_FORMAT_FLOAT) return AUDIO3D_ERROR_INVALID_PARAMETER; + if (output_route != AUDIO3D_OUTPUT_ROUTE_BOTH && output_route != AUDIO3D_OUTPUT_ROUTE_HMU_ONLY && output_route != AUDIO3D_OUTPUT_ROUTE_TV_ONLY) return AUDIO3D_ERROR_INVALID_PARAMETER; + if (buffer == nullptr || num_samples == 0) return AUDIO3D_ERROR_INVALID_PARAMETER; + const std::size_t sample_size = format == AUDIO3D_FORMAT_S16 ? 2u : 4u; + if (reinterpret_cast(buffer) % sample_size != 0) return AUDIO3D_ERROR_INVALID_PARAMETER; + const std::uint32_t samples = std::min(num_samples, g_port.granularity); + const std::size_t bytes = static_cast(samples) * num_channels * sample_size; + if (g_port.beds.size() >= g_port.queue_depth) return AUDIO3D_ERROR_NOT_READY; + const auto* first = static_cast(buffer); + Audio3dBed bed; + bed.pcm.assign(first, first + bytes); + bed.channels = num_channels; + bed.format = format; + bed.samples = samples; + g_port.beds.push_back(std::move(bed)); + return 0; +} + +int APS5_VABI sceAudio3dBedWrite(uint32_t port_id, uint32_t num_channels, uint32_t format, const void* buffer, uint32_t num_samples) { + return sceAudio3dBedWrite2(port_id, num_channels, format, buffer, num_samples, AUDIO3D_OUTPUT_ROUTE_BOTH, false); +} + +int APS5_VABI sceAudio3dAudioOutOpen(uint32_t port_id, int user_id, std::int32_t type, std::int32_t index, uint32_t len, uint32_t freq, uint32_t param) { + { + std::lock_guard lock(g_mutex); + if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; + if (len != g_port.granularity) return AUDIO3D_ERROR_INVALID_PARAMETER; + if (index != 0 || (param & 0xFF) > 7) return AUDIO3D_ERROR_INVALID_PARAMETER; + } + const int handle = sceAudioOutOpen(user_id, type, index, len, freq, param); + if (handle < 0) return AUDIO3D_ERROR_OUT_OF_RESOURCES; + bool registered = false; + { + std::lock_guard lock(g_mutex); + if (PortIsOpen(port_id)) { + g_port.audio_out_ports.emplace(handle, Audio3dAudioOut{AudioOutBufferBytes(len, param)}); + registered = true; + } + } + if (!registered) { + sceAudioOutClose(handle); + return AUDIO3D_ERROR_INVALID_PORT; + } + return handle; +} + +int APS5_VABI sceAudio3dAudioOutClose(std::int32_t handle) { + { + std::lock_guard lock(g_mutex); + if (g_port.audio_out_ports.erase(handle) == 0) return AUDIO3D_ERROR_INVALID_PORT; + } + sceAudioOutClose(handle); + return 0; +} + +int APS5_VABI sceAudio3dAudioOutOutput(std::int32_t handle, const void* ptr) { + std::lock_guard lock(g_mutex); + const auto port = g_port.audio_out_ports.find(handle); + if (port == g_port.audio_out_ports.end()) return AUDIO3D_ERROR_INVALID_PORT; + if (ptr == nullptr) return AUDIO3D_ERROR_INVALID_PARAMETER; + if (port->second.pending.size() >= g_port.queue_depth) return AUDIO3D_ERROR_NOT_READY; + const auto* first = static_cast(ptr); + port->second.pending.emplace_back(first, first + port->second.buffer_bytes); + return static_cast(g_port.granularity); +} + +int APS5_VABI sceAudio3dAudioOutOutputs(AudioOutOutputParam* param, uint32_t num) { + if (param == nullptr || num == 0) return AUDIO3D_ERROR_INVALID_PARAMETER; + for (std::uint32_t i = 0; i < num; ++i) { + const int result = sceAudio3dAudioOutOutput(param[i].handle, param[i].ptr); + if (result < 0) return result; + } + return 0; +} + +int APS5_VABI sceAudio3dStrError(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudio3dPortOpenEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudio3dPortCreate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudio3dPortDestroy(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudio3dPortGetStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudio3dReportRegisterHandler(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudio3dReportUnregisterHandler(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudio3dGetSpeakerArrayMemorySize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudio3dCreateSpeakerArray(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudio3dDeleteSpeakerArray(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudio3dGetSpeakerArrayMixCoefficients(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudio3dGetSpeakerArrayMixCoefficients2(void) { + NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceAudio3d/Mixer.hpp b/core/libs/prx/libSceAudio3d/Mixer.hpp new file mode 100644 index 0000000000..569d653e19 --- /dev/null +++ b/core/libs/prx/libSceAudio3d/Mixer.hpp @@ -0,0 +1,70 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAUDIO3D_MIXER_HPP +#define CORE_LIBS_PRX_LIBSCEAUDIO3D_MIXER_HPP + +#include +#include +#include +#include +#include + +namespace Audio3dInternal { + +struct Source { + const void* samples; + std::uint32_t num_samples; + std::uint32_t channels; + bool is_float; + float gain; + float pan; +}; + +constexpr float kSurroundFold = 0.7071f; +constexpr std::uint32_t kChannelL = 0; +constexpr std::uint32_t kChannelR = 1; +constexpr std::uint32_t kChannelCenter = 2; +[[maybe_unused]] constexpr std::uint32_t kChannelLfe = 3; +constexpr std::uint32_t kChannelLs = 4; +constexpr std::uint32_t kChannelRs = 5; +constexpr std::uint32_t kChannelLrs = 6; +constexpr std::uint32_t kChannelRrs = 7; + +inline float ReadSample(const Source& source, std::uint32_t frame, std::uint32_t channel) { + const std::size_t index = static_cast(frame) * source.channels + channel; + if (source.is_float) return static_cast(source.samples)[index]; + return static_cast(static_cast(source.samples)[index]) / 32768.0f; +} + +inline void MixFrame(const Source* sources, std::size_t count, std::uint32_t granularity, std::vector& out) { + std::vector mix(static_cast(granularity) * 2, 0.0f); + for (std::size_t source_index = 0; source_index < count; ++source_index) { + const Source& src = sources[source_index]; + if (src.samples == nullptr || src.num_samples == 0) continue; + const float pan = std::clamp(src.pan, -1.0f, 1.0f); + const float gl = (pan <= 0.0f ? 1.0f : 1.0f - pan) * src.gain; + const float gr = (pan >= 0.0f ? 1.0f : 1.0f + pan) * src.gain; + for (std::uint32_t frame = 0; frame < granularity && frame < src.num_samples; ++frame) { + const std::size_t left_index = static_cast(frame) * 2; + float left = ReadSample(src, frame, kChannelL); + float right = src.channels == 1 ? left : ReadSample(src, frame, kChannelR); + if (src.channels > kChannelCenter) { + const float center = ReadSample(src, frame, kChannelCenter) * kSurroundFold; + left += center; + right += center; + } + if (src.channels > kChannelLs) left += ReadSample(src, frame, kChannelLs) * kSurroundFold; + if (src.channels > kChannelRs) right += ReadSample(src, frame, kChannelRs) * kSurroundFold; + if (src.channels > kChannelLrs) left += ReadSample(src, frame, kChannelLrs) * kSurroundFold; + if (src.channels > kChannelRrs) right += ReadSample(src, frame, kChannelRrs) * kSurroundFold; + mix[left_index] += left * gl; + mix[left_index + 1] += right * gr; + } + } + out.resize(static_cast(granularity) * 2); + for (std::size_t i = 0; i < out.size(); ++i) { + out[i] = static_cast(std::lround(std::clamp(mix[i], -1.0f, 1.0f) * 32767.0f)); + } +} + +} + +#endif diff --git a/core/libs/prx/libSceAudioIn/Export.cpp b/core/libs/prx/libSceAudioIn/Export.cpp index c21557a76f..68fa257250 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,175 @@ 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; +} + +int APS5_VABI sceAudioInDeviceIdOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInDeviceIdAsyncOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetUsbGain(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInIsSharedDevice(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInDeviceIdHqOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInDeviceHqOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetConnections(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetPortConnections(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetPortStatuses(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInGetHandleStatusInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInOpenEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInHqOpenEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInDeviceOpenEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInChangeAppModuleState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetSparkParam(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetSparkSideTone(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetCompressorPreGain(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetNoiseGateThreshold(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetMicLevel(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetDeviceMuteStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetDevConnection(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInCountPorts(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetUserMute(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInVmicCreate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInVmicDestroy(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInVmicWrite(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInVmicGetStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInVmicSetMute(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetMode2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInExtCtrl(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInInputs(void) { + NotImplemented_nid_no_patch(__func__); + return 0; } } 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..af79a20cf1 100644 --- a/core/libs/prx/libSceAudioOut/CMakeLists.txt +++ b/core/libs/prx/libSceAudioOut/CMakeLists.txt @@ -3,12 +3,14 @@ add_library(libSceAudioOut SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOutSdl.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOutShutdown.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut2Context.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut2Low.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut2PadMix.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut2Port.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut2SpeakerArray.cpp ${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/AudioOut2Low.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Low.cpp new file mode 100644 index 0000000000..eaba6e102d --- /dev/null +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Low.cpp @@ -0,0 +1,69 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceAudioOut2Initialize(void); +int APS5_VABI sceAudioOut2ContextQueryMemory(const AudioOut2ContextParam* params, size_t* memory_size); +int APS5_VABI sceAudioOut2ContextCreate(const AudioOut2ContextParam* params, void* buffer, size_t buffer_size, AudioOut2ContextHandle* ctx); +int APS5_VABI sceAudioOut2ContextDestroy(AudioOut2ContextHandle ctx); +int APS5_VABI sceAudioOut2ContextSetAttributes(AudioOut2ContextHandle ctx, const AudioOut2Attribute* attributes, uint32_t num); +int APS5_VABI sceAudioOut2ContextAdvance(AudioOut2ContextHandle ctx); +int APS5_VABI sceAudioOut2ContextPush(AudioOut2ContextHandle ctx, uint32_t blocking); +int APS5_VABI sceAudioOut2ContextGetQueueLevel(AudioOut2ContextHandle ctx, uint32_t* queue_level, uint32_t* available_queue); +int APS5_VABI sceAudioOut2PortCreate(AudioOut2ContextHandle ctx, const AudioOut2PortParam* params, AudioOut2PortHandle* port); +int APS5_VABI sceAudioOut2PortDestroy(AudioOut2PortHandle port); +int APS5_VABI sceAudioOut2PortSetAttributes(AudioOut2PortHandle port, const AudioOut2Attribute* attributes, uint32_t num); +int APS5_VABI sceAudioOut2PortGetState(AudioOut2PortHandle port, AudioOut2PortState* state); + +int APS5_VABI sceAudioOut2LoInit(void) { + return sceAudioOut2Initialize(); +} + +int APS5_VABI sceAudioOut2LoContextQueryMemory(const AudioOut2ContextParam* params, size_t* memory_size) { + return sceAudioOut2ContextQueryMemory(params, memory_size); +} + +int APS5_VABI sceAudioOut2LoContextCreate(const AudioOut2ContextParam* params, void* buffer, size_t buffer_size, AudioOut2ContextHandle* ctx) { + return sceAudioOut2ContextCreate(params, buffer, buffer_size, ctx); +} + +int APS5_VABI sceAudioOut2LoContextDestroy(AudioOut2ContextHandle ctx) { + return sceAudioOut2ContextDestroy(ctx); +} + +int APS5_VABI sceAudioOut2LoContextSetAttributes(AudioOut2ContextHandle ctx, const AudioOut2Attribute* attributes, uint32_t num) { + return sceAudioOut2ContextSetAttributes(ctx, attributes, num); +} + +int APS5_VABI sceAudioOut2LoContextAdvance(AudioOut2ContextHandle ctx) { + return sceAudioOut2ContextAdvance(ctx); +} + +int APS5_VABI sceAudioOut2LoContextPush(AudioOut2ContextHandle ctx, uint32_t blocking) { + return sceAudioOut2ContextPush(ctx, blocking); +} + +int APS5_VABI sceAudioOut2LoContextGetQueueLevel(AudioOut2ContextHandle ctx, uint32_t* queue_level, uint32_t* available_queue) { + return sceAudioOut2ContextGetQueueLevel(ctx, queue_level, available_queue); +} + +int APS5_VABI sceAudioOut2LoPortCreate(AudioOut2ContextHandle ctx, const AudioOut2PortParam* params, AudioOut2PortHandle* port) { + return sceAudioOut2PortCreate(ctx, params, port); +} + +int APS5_VABI sceAudioOut2LoPortDestroy(AudioOut2PortHandle port) { + return sceAudioOut2PortDestroy(port); +} + +int APS5_VABI sceAudioOut2LoPortSetAttributes(AudioOut2PortHandle port, const AudioOut2Attribute* attributes, uint32_t num) { + return sceAudioOut2PortSetAttributes(port, attributes, num); +} + +int APS5_VABI sceAudioOut2LoPortGetState(AudioOut2PortHandle port, AudioOut2PortState* state) { + return sceAudioOut2PortGetState(port, state); +} + +} diff --git a/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp b/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp new file mode 100644 index 0000000000..02abcf4fd9 --- /dev/null +++ b/core/libs/prx/libSceAudioOut/src/Unimplemented.cpp @@ -0,0 +1,428 @@ +#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 sceAudioOutOpenEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioOutSetSystemDebugState(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 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 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 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/libSceAudioOut/tests/AudioOut2Latency.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp index 6ed601552a..9a09ec4fba 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp @@ -9,6 +9,8 @@ extern "C" { int APS5_VABI sceAudioOut2Initialize(); +int APS5_VABI sceAudioOut2LoInit(); +int APS5_VABI sceAudioOut2LoTerminate(); int APS5_VABI sceAudioOut2Set3DLatency(int, std::uint32_t); int APS5_VABI sceAudioOut2MasteringInit(std::uint32_t); int APS5_VABI sceAudioOut2MasteringTerm(); @@ -117,6 +119,12 @@ void TestEnableChat() { Require(sceAudioOut2EnableChat() == 0, "enabling chat after disabling it must be accepted"); } +void TestLowLatencyService() { + Require(sceAudioOut2LoInit() == 0, "starting the low-latency service must be accepted"); + Require(sceAudioOut2LoInit() == 0, "starting the low-latency service again must be accepted"); + Require(ThrowsRuntimeError([] { sceAudioOut2LoTerminate(); }), "stopping the low-latency service must throw"); +} + } int main() { @@ -128,5 +136,6 @@ int main() { TestMasteringGetState(); TestSetSystemDebugState(); TestEnableChat(); + TestLowLatencyService(); return 0; } diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOut2Timing.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOut2Timing.cpp index c16ffebbfb..36074daece 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOut2Timing.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOut2Timing.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -15,6 +16,7 @@ extern "C" { int APS5_VABI sceAudioOut2ContextResetParam(AudioOut2ContextParam*); +int APS5_VABI sceAudioOut2ContextQueryMemory(const AudioOut2ContextParam*, std::size_t*); int APS5_VABI sceAudioOut2ContextCreate(const AudioOut2ContextParam*, void*, std::size_t, AudioOut2ContextHandle*); int APS5_VABI sceAudioOut2ContextDestroy(AudioOut2ContextHandle); int APS5_VABI sceAudioOut2ContextPush(AudioOut2ContextHandle, std::uint32_t); @@ -22,6 +24,17 @@ int APS5_VABI sceAudioOut2ContextGetQueueLevel(AudioOut2ContextHandle, std::uint int APS5_VABI sceAudioOut2PortCreate(AudioOut2ContextHandle, const AudioOut2PortParam*, AudioOut2PortHandle*); int APS5_VABI sceAudioOut2PortDestroy(AudioOut2PortHandle); int APS5_VABI sceAudioOut2PortSetAttributes(AudioOut2PortHandle, const AudioOut2Attribute*, std::uint32_t); +int APS5_VABI sceAudioOut2LoContextQueryMemory(const AudioOut2ContextParam*, std::size_t*); +int APS5_VABI sceAudioOut2LoContextCreate(const AudioOut2ContextParam*, void*, std::size_t, AudioOut2ContextHandle*); +int APS5_VABI sceAudioOut2LoContextDestroy(AudioOut2ContextHandle); +int APS5_VABI sceAudioOut2LoContextSetAttributes(AudioOut2ContextHandle, const AudioOut2Attribute*, std::uint32_t); +int APS5_VABI sceAudioOut2LoContextAdvance(AudioOut2ContextHandle); +int APS5_VABI sceAudioOut2LoContextPush(AudioOut2ContextHandle, std::uint32_t); +int APS5_VABI sceAudioOut2LoContextGetQueueLevel(AudioOut2ContextHandle, std::uint32_t*, std::uint32_t*); +int APS5_VABI sceAudioOut2LoPortCreate(AudioOut2ContextHandle, const AudioOut2PortParam*, AudioOut2PortHandle*); +int APS5_VABI sceAudioOut2LoPortDestroy(AudioOut2PortHandle); +int APS5_VABI sceAudioOut2LoPortSetAttributes(AudioOut2PortHandle, const AudioOut2Attribute*, std::uint32_t); +int APS5_VABI sceAudioOut2LoPortGetState(AudioOut2PortHandle, AudioOut2PortState*); } static void Require(bool value) { if (!value) std::abort(); } @@ -44,6 +57,8 @@ constexpr std::uint32_t formatMonoFloat = 1u << 8; constexpr std::uint32_t formatStereoFloat = 2u << 8; constexpr std::uint32_t attributeData = 0; constexpr int queueFull = static_cast(0x80260507); +constexpr int errorInvalidArgument = static_cast(0x80260502); +constexpr int errorInvalidHandle = static_cast(0x80260503); constexpr float masterGain = 0.5f; constexpr float tolerance = 1e-5f; @@ -53,24 +68,26 @@ struct Session { AudioOut2PortHandle port = 0; std::vector buffer = std::vector(static_cast(grain) * 2, 0.0f); - explicit Session(const char* name) : path(std::filesystem::temp_directory_path() / (std::to_string(std::random_device{}()) + "-" + name)) { + bool low; + + explicit Session(const char* name, bool lowLatency = false) : path(std::filesystem::temp_directory_path() / (std::to_string(std::random_device{}()) + "-" + name)), low(lowLatency) { std::filesystem::remove(path); SetEnvironment("SDL_DISKAUDIOFILE", path.string()); AudioOut2ContextParam params{}; Require(sceAudioOut2ContextResetParam(¶ms) == 0); params.num_grains = grain; params.queue_depth = queueDepth; - Require(sceAudioOut2ContextCreate(¶ms, nullptr, 0, &context) == 0); + Require((low ? sceAudioOut2LoContextCreate : sceAudioOut2ContextCreate)(¶ms, nullptr, 0, &context) == 0); AudioOut2PortParam port{}; port.port_type = 0; port.data_format = formatStereoFloat; port.sampling_freq = frequency; - Require(sceAudioOut2PortCreate(context, &port, &this->port) == 0); + Require((low ? sceAudioOut2LoPortCreate : sceAudioOut2PortCreate)(context, &port, &this->port) == 0); } void Point(const void* data) { const AudioOut2Attribute attribute{attributeData, 0, &data, sizeof(data)}; - Require(sceAudioOut2PortSetAttributes(port, &attribute, 1) == 0); + Require((low ? sceAudioOut2LoPortSetAttributes : sceAudioOut2PortSetAttributes)(port, &attribute, 1) == 0); } void Fill(float value) { @@ -78,8 +95,8 @@ struct Session { } std::vector Close() { - Require(sceAudioOut2PortDestroy(port) == 0); - Require(sceAudioOut2ContextDestroy(context) == 0); + Require((low ? sceAudioOut2LoPortDestroy : sceAudioOut2PortDestroy)(port) == 0); + Require((low ? sceAudioOut2LoContextDestroy : sceAudioOut2ContextDestroy)(context) == 0); std::ifstream file(path, std::ios::binary); const std::vector bytes{std::istreambuf_iterator(file), std::istreambuf_iterator()}; file.close(); @@ -188,6 +205,132 @@ void TestPrimingAfterRunningDry() { Require(Constant(played, second, burst, -0.5f * masterGain)); } +void TestLowLatencyCallsRejectBadArguments() { + AudioOut2ContextParam params{}; + Require(sceAudioOut2ContextResetParam(¶ms) == 0); + params.num_grains = grain; + params.queue_depth = queueDepth; + const std::size_t sizeSentinel = 0x1234; + std::size_t size = sizeSentinel; + Require(sceAudioOut2LoContextQueryMemory(nullptr, &size) == errorInvalidArgument); + Require(sceAudioOut2LoContextQueryMemory(¶ms, nullptr) == errorInvalidArgument); + Require(size == sizeSentinel); + Require(sceAudioOut2LoContextQueryMemory(¶ms, &size) == 0); + std::size_t standardSize = sizeSentinel; + Require(sceAudioOut2ContextQueryMemory(¶ms, &standardSize) == 0); + Require(size == standardSize && size != sizeSentinel); + + const AudioOut2ContextHandle contextSentinel = 0x5A5A; + AudioOut2ContextHandle context = contextSentinel; + Require(sceAudioOut2LoContextCreate(nullptr, nullptr, 0, &context) == errorInvalidArgument); + Require(context == contextSentinel); + Require(sceAudioOut2LoContextCreate(¶ms, nullptr, 0, nullptr) == errorInvalidArgument); + + Require(sceAudioOut2LoContextDestroy(0) == errorInvalidHandle); + Require(sceAudioOut2LoContextAdvance(0) == errorInvalidHandle); + Require(sceAudioOut2LoContextPush(0, 0) == errorInvalidHandle); + Require(sceAudioOut2LoContextPush(0, 1) == errorInvalidHandle); + std::uint32_t level = 77; + std::uint32_t available = 78; + Require(sceAudioOut2LoContextGetQueueLevel(0, &level, &available) == errorInvalidHandle); + Require(level == 77 && available == 78); + const AudioOut2Attribute attribute{0, 0, nullptr, 0}; + Require(sceAudioOut2LoContextSetAttributes(0, &attribute, 1) == errorInvalidHandle); + + AudioOut2PortParam portParams{}; + portParams.port_type = 0; + portParams.data_format = formatStereoFloat; + portParams.sampling_freq = frequency; + const AudioOut2PortHandle portSentinel = 0x6B6B; + AudioOut2PortHandle port = portSentinel; + Require(sceAudioOut2LoPortCreate(0, &portParams, &port) == errorInvalidHandle); + Require(port == portSentinel); + Require(sceAudioOut2LoPortDestroy(0) == errorInvalidHandle); + Require(sceAudioOut2LoPortSetAttributes(0, &attribute, 1) == errorInvalidHandle); + AudioOut2PortState state; + Require(sceAudioOut2LoPortGetState(0, &state) == errorInvalidHandle); + + Session session("anyps5_audio_out2_low_arguments.raw", true); + Require(sceAudioOut2LoPortCreate(session.context, nullptr, &port) == errorInvalidArgument); + Require(port == portSentinel); + Require(sceAudioOut2LoPortCreate(session.context, &portParams, nullptr) == errorInvalidArgument); + Require(sceAudioOut2LoContextSetAttributes(session.context, nullptr, 1) == errorInvalidArgument); + Require(sceAudioOut2LoContextSetAttributes(session.context, nullptr, 0) == 0); + Require(sceAudioOut2LoContextSetAttributes(session.context, &attribute, 1) == 0); + Require(sceAudioOut2LoPortSetAttributes(session.port, nullptr, 1) == errorInvalidArgument); + Require(sceAudioOut2LoPortSetAttributes(session.port, nullptr, 0) == 0); + Require(sceAudioOut2LoPortGetState(session.port, nullptr) == errorInvalidArgument); + Require(sceAudioOut2LoContextGetQueueLevel(session.context, nullptr, nullptr) == 0); + std::memset(&state, 0xAB, sizeof(state)); + Require(sceAudioOut2LoPortGetState(session.port, &state) == 0); + Require(state.output == 1 && state.num_channels == 2 && state.volume == 127 && state.reroute_counter == 0 && state.flags == 0); + const auto destroyed = session.port; + Require(sceAudioOut2LoPortDestroy(destroyed) == 0); + Require(sceAudioOut2LoPortDestroy(destroyed) == errorInvalidHandle); + Require(sceAudioOut2LoPortGetState(destroyed, &state) == errorInvalidHandle); + Require(sceAudioOut2LoPortSetAttributes(destroyed, &attribute, 1) == errorInvalidHandle); + Require(sceAudioOut2LoContextDestroy(session.context) == 0); + std::filesystem::remove(session.path); +} + +void TestLowLatencyReadsAtNextPush() { + Session session("anyps5_audio_out2_low_next_push.raw", true); + session.Fill(0.25f); + session.Point(session.buffer.data()); + Require(sceAudioOut2LoContextPush(session.context, 1) == 0); + session.Fill(0.5f); + Require(sceAudioOut2LoContextPush(session.context, 1) == 0); + session.Fill(0.75f); + Require(sceAudioOut2LoContextPush(session.context, 1) == 0); + session.Fill(0.0f); + session.Point(nullptr); + for (std::uint32_t push = 0; push < cushionGrains * 4; push++) Require(sceAudioOut2LoContextPush(session.context, 1) == 0); + const auto played = session.Close(); + const auto first = FirstNonZero(played); + const std::size_t samples = static_cast(grain) * 2; + Require(Constant(played, first, samples, 0.5f * masterGain)); + Require(Constant(played, first + samples, samples, 0.75f * masterGain)); + Require(FirstNonZero(played, first + 2 * samples) == played.size()); +} + +void TestLowLatencyAdvanceKeepsTheAdvancedGrain() { + Session session("anyps5_audio_out2_low_advance.raw", true); + session.Fill(0.25f); + session.Point(session.buffer.data()); + Require(sceAudioOut2LoContextAdvance(session.context) == 0); + session.Fill(0.75f); + session.Point(nullptr); + for (std::uint32_t push = 0; push < cushionGrains * 4; push++) { + Require(sceAudioOut2LoContextPush(session.context, 1) == 0); + Require(sceAudioOut2LoContextAdvance(session.context) == 0); + } + const auto played = session.Close(); + const auto first = FirstNonZero(played); + const std::size_t samples = static_cast(grain) * 2; + Require(Constant(played, first, samples, 0.25f * masterGain)); + Require(FirstNonZero(played, first + samples) == played.size()); +} + +void TestLowLatencyQueueLevelFillsToTheDepth() { + Session session("anyps5_audio_out2_low_level.raw", true); + session.Point(nullptr); + std::uint32_t level = 99; + std::uint32_t available = 99; + Require(sceAudioOut2LoContextGetQueueLevel(session.context, &level, &available) == 0); + Require(level == 0 && available == queueDepth); + std::uint32_t accepted = 0; + int result = 0; + while ((result = sceAudioOut2LoContextPush(session.context, 0)) == 0) { + accepted++; + Require(accepted < 1000); + } + Require(result == queueFull); + Require(accepted >= cushionGrains + queueDepth); + Require(sceAudioOut2LoContextGetQueueLevel(session.context, &level, &available) == 0); + Require(level <= queueDepth && level + available == queueDepth); + session.Close(); +} + } int main() { @@ -196,5 +339,9 @@ int main() { TestRecreatedPort(); TestLevelExcludesCushion(); TestPrimingAfterRunningDry(); + TestLowLatencyCallsRejectBadArguments(); + TestLowLatencyReadsAtNextPush(); + TestLowLatencyAdvanceKeepsTheAdvancedGrain(); + TestLowLatencyQueueLevelFillsToTheDepth(); return 0; } diff --git a/core/libs/prx/libSceAudioPropagation/Export.cpp b/core/libs/prx/libSceAudioPropagation/Export.cpp index 64aada2e47..474117fe6f 100644 --- a/core/libs/prx/libSceAudioPropagation/Export.cpp +++ b/core/libs/prx/libSceAudioPropagation/Export.cpp @@ -312,7 +312,6 @@ std::int32_t APS5_VABI sceAudioPropagationSourceRender(AudioPropagationHandle sy require(info.output != nullptr && info.outputSize != 0, __func__); if (info.format != RenderFormat) throw std::runtime_error(std::string(__func__) + ": render format " + std::to_string(info.format) + " is unknown"); } - for (std::uint32_t index = 0; index < count; ++index) std::memset(infos[index].output, 0, infos[index].outputSize); return 0; } diff --git a/core/libs/prx/libSceAudiodec.native/Export.cpp b/core/libs/prx/libSceAudiodec.native/Export.cpp new file mode 100644 index 0000000000..4f00015020 --- /dev/null +++ b/core/libs/prx/libSceAudiodec.native/Export.cpp @@ -0,0 +1,78 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceAudiodecClearContextEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudiodecCreateDecoderEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudiodecDecode2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudiodecDecode2Ex(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudiodecDecode2WithPriority(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudiodecDecode2WithPriorityEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudiodecDecodeEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudiodecDecodeWithPriority(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudiodecDecodeWithPriorityEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudiodecDeleteDecoderEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudiodecGetContext(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudiodecInitialize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudiodecRegisterCodec(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudiodecTerminate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceAudiodec/Export.cpp b/core/libs/prx/libSceAudiodec/Export.cpp index fc1a113e4f..ffc1d6e1f7 100644 --- a/core/libs/prx/libSceAudiodec/Export.cpp +++ b/core/libs/prx/libSceAudiodec/Export.cpp @@ -66,9 +66,9 @@ std::int32_t wordSizeOf(const void* param) { return static_cast(param)->i_bw_pcm; } -std::int32_t validateWordSize(std::int32_t wordSize) { - if (wordSize == Audiodec::WORD_SIZE_16BIT || wordSize == Audiodec::WORD_SIZE_FLOAT) return 0; - if (wordSize == 0) NotImplemented_nid_no_patch("libSceAudiodec 24-bit PCM output"); +std::int32_t validateWordSize(std::int32_t wordSize, std::uint32_t codecType) { + if (wordSize == Audiodec::WORD_SIZE_16BIT) return 0; + if (wordSize == Audiodec::WORD_SIZE_FLOAT && codecType != TYPE_MP3) return 0; return ERROR_INVALID_WORD_LENGTH; } @@ -92,7 +92,7 @@ std::int32_t validateCtrl(const AudiodecCtrl* ctrl, std::uint32_t codecType) { if (infoSize != sizeof(AudiodecM4aacInfo)) return ERROR_INVALID_BSI_INFO_SIZE; break; } - return validateWordSize(wordSizeOf(ctrl->pParam)); + return validateWordSize(wordSizeOf(ctrl->pParam), codecType); } std::int32_t validateBuffers(const AudiodecCtrl* ctrl) { diff --git a/core/libs/prx/libSceAudiodecCpu/Export.cpp b/core/libs/prx/libSceAudiodecCpu/Export.cpp new file mode 100644 index 0000000000..c0c2f83744 --- /dev/null +++ b/core/libs/prx/libSceAudiodecCpu/Export.cpp @@ -0,0 +1,48 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceAudiodecCpuClearContext(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudiodecCpuDecode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudiodecCpuInitDecoder(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudiodecCpuInternalClearContext(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudiodecCpuInternalDecode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudiodecCpuInternalInitDecoder(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudiodecCpuInternalQueryMemSize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudiodecCpuQueryMemSize(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/libSceAvPlayer.native/Export.cpp b/core/libs/prx/libSceAvPlayer.native/Export.cpp new file mode 100644 index 0000000000..2584dda091 --- /dev/null +++ b/core/libs/prx/libSceAvPlayer.native/Export.cpp @@ -0,0 +1,18 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceAvPlayerPrintf(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvPlayerVprintf(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceAvPlayer/CMakeLists.txt b/core/libs/prx/libSceAvPlayer/CMakeLists.txt index 0acb2eeeb0..d8a652d7ff 100644 --- a/core/libs/prx/libSceAvPlayer/CMakeLists.txt +++ b/core/libs/prx/libSceAvPlayer/CMakeLists.txt @@ -12,6 +12,11 @@ if(BUILD_TESTING) file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/guest_avplayer) add_test(NAME guest_avplayer COMMAND guest_avplayer_tests) add_test(NAME guest_avplayer_callback_close COMMAND guest_avplayer_tests --callback-close) + add_test(NAME guest_avplayer_paused_first_video COMMAND guest_avplayer_tests --paused-first-video) + set_tests_properties(guest_avplayer_paused_first_video PROPERTIES + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/guest_avplayer_paused_first_video + TIMEOUT 20) + file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/guest_avplayer_paused_first_video) set_tests_properties(guest_avplayer PROPERTIES WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/guest_avplayer TIMEOUT 60) @@ -21,6 +26,8 @@ if(BUILD_TESTING) if(WIN32) set_tests_properties(guest_avplayer PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(guest_avplayer_paused_first_video PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") set_tests_properties(guest_avplayer_callback_close PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() diff --git a/core/libs/prx/libSceAvPlayer/Export.cpp b/core/libs/prx/libSceAvPlayer/Export.cpp index 59ce8e72a8..2cdb89887d 100644 --- a/core/libs/prx/libSceAvPlayer/Export.cpp +++ b/core/libs/prx/libSceAvPlayer/Export.cpp @@ -191,5 +191,13 @@ int APS5_VABI sceAvPlayerStreamCount(AvPlayerInternal* h) { return ToPlayer(h)->StreamCount(); } +} + +namespace AvPlayer { + +void SetAvPlayerClockForTest_nid_no_patch(AvPlayerInternal* h, double milliseconds) { + if (h) ToPlayer(h)->SetClockTimeForTest(milliseconds); +} + } #pragma GCC visibility pop diff --git a/core/libs/prx/libSceAvPlayer/include/AvPlayer.hpp b/core/libs/prx/libSceAvPlayer/include/AvPlayer.hpp index 97d3be2383..49bf029391 100644 --- a/core/libs/prx/libSceAvPlayer/include/AvPlayer.hpp +++ b/core/libs/prx/libSceAvPlayer/include/AvPlayer.hpp @@ -79,12 +79,15 @@ public: virtual bool GetAudioData(AvPlayerFrameInfo& info) = 0; virtual std::uint64_t CurrentTime() = 0; virtual bool Finished() = 0; + virtual void SetClockTimeForTest(double milliseconds) = 0; }; std::unique_ptr OpenSource(const SourceSettings& settings, const std::string& path, ISourceEvents& events); std::uint32_t DetectSourceType(std::string_view path); +void SetAvPlayerClockForTest_nid_no_patch(AvPlayerInternal* h, double milliseconds); + class Player final : public AvPlayerInternal, private ISourceEvents { public: Player(const AvPlayerInitData& init, std::uint32_t bufferCount, bool ps5Layout = false); @@ -113,6 +116,7 @@ public: bool GetAudioData(AvPlayerFrameInfo& info); bool IsActive(); std::uint64_t CurrentTime(); + void SetClockTimeForTest(double milliseconds); private: enum class State { Initial, Ready, Play, Pause, Stop, EndOfFile, Error }; diff --git a/core/libs/prx/libSceAvPlayer/src/Player.cpp b/core/libs/prx/libSceAvPlayer/src/Player.cpp index cf28da31f7..191a185847 100644 --- a/core/libs/prx/libSceAvPlayer/src/Player.cpp +++ b/core/libs/prx/libSceAvPlayer/src/Player.cpp @@ -386,4 +386,9 @@ std::uint64_t Player::CurrentTime() { return source ? source->CurrentTime() : 0; } +void Player::SetClockTimeForTest(double milliseconds) { + std::lock_guard lock(mutex); + if (source) source->SetClockTimeForTest(milliseconds); +} + } diff --git a/core/libs/prx/libSceAvPlayer/src/Source.cpp b/core/libs/prx/libSceAvPlayer/src/Source.cpp index b9c1e162f3..70f7e7fd31 100644 --- a/core/libs/prx/libSceAvPlayer/src/Source.cpp +++ b/core/libs/prx/libSceAvPlayer/src/Source.cpp @@ -137,11 +137,16 @@ private: class Clock { public: double Now() const { + if (overrideTime >= 0.0) return overrideTime; if (!running || paused) return base; const std::chrono::duration elapsed = std::chrono::steady_clock::now() - wall; return std::max(0.0, base + elapsed.count() * speed / NormalSpeed); } + void SetOverrideForTest(double milliseconds) { + overrideTime = milliseconds; + } + void Rebase(double milliseconds) { base = milliseconds; wall = std::chrono::steady_clock::now(); @@ -176,6 +181,7 @@ private: std::int32_t speed = NormalSpeed; bool running = false; bool paused = false; + double overrideTime = -1.0; }; enum class PacketKind { Data, Flush, Drain, End, Reopen }; @@ -432,6 +438,11 @@ public: condition.notify_all(); } + void SetClockTimeForTest(double milliseconds) override { + std::lock_guard lock(mutex); + presentationClock.SetOverrideForTest(milliseconds); + } + bool GetVideoData(AvPlayerFrameInfoEx& info) override { bool taken = false; { @@ -507,7 +518,7 @@ private: while (!frames.empty() && frames.front().epoch < stale) recycleFront(video); if (frames.empty()) return false; const auto& front = frames.front(); - if (paused && front.epoch <= clockEpoch) return false; + if (paused && presented && front.epoch <= clockEpoch) return false; if (front.epoch > clockEpoch) { if (synced && audioDriving && speed == NormalSpeed) return false; if (synced && front.seamless && presented && presentationClock.Now() < static_cast(lastPresented + frameDuration())) return false; @@ -516,7 +527,8 @@ private: } if (synced || audioDriving) { const auto now = presentationClock.Now(); - if (synced && static_cast(front.info.timestamp) > now) return false; + const double syncTolerance = static_cast(frameDuration()) * 0.75; + if (synced && static_cast(front.info.timestamp) > now + syncTolerance) return false; while (frames.size() > 1 && frames[1].epoch == frames.front().epoch && static_cast(frames[1].info.timestamp) <= now) recycleFront(video); } present(video, info); diff --git a/core/libs/prx/libSceAvPlayerStreaming/Export.cpp b/core/libs/prx/libSceAvPlayerStreaming/Export.cpp index d81f4a5e32..b484bc1af2 100644 --- a/core/libs/prx/libSceAvPlayerStreaming/Export.cpp +++ b/core/libs/prx/libSceAvPlayerStreaming/Export.cpp @@ -6,5 +6,169 @@ extern "C" { APS5_DUMMY_FUN +int APS5_VABI MvpHttpAbort_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI MvpHttpDeInit_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI MvpHttpGet_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI MvpHttpGetLastError_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI MvpHttpHead_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI MvpHttpHeadCleanup_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI MvpHttpInit_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI MvpHttpPost_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI MvpHttpSetCert_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI MvpHttpSetRecvTimeout_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI MvpHttpSetRedirectCb_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTsCloseFile(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTsDisableRepresentation(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTsEnableRepresentation(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTsEnableStream(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTsGetAvailableBW(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTsGetLiveStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTsGetNextAuInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTsGetNextUnitData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTsGetRepresentationCount(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTsGetRepresentationInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTsGetSeekWindow(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTsGetStreamInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTsGetUrlEncryptionType(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTsJumpPTS(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTsOpenFile(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTsOpenFileEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTsReleaseBuffer(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTsRepresentationIsEnabled(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTsSetAvailableBW(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTsSetStartingBW(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTsStartFileStreaming(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceTsStopFileStreaming(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceBeisobmf/Export.cpp b/core/libs/prx/libSceBeisobmf/Export.cpp index d81f4a5e32..a2eee1a93d 100644 --- a/core/libs/prx/libSceBeisobmf/Export.cpp +++ b/core/libs/prx/libSceBeisobmf/Export.cpp @@ -6,5 +6,94 @@ extern "C" { APS5_DUMMY_FUN +int APS5_VABI isobmf_box_getchildbox_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI isobmf_box_getdiskoffset_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI isobmf_box_getflags32b_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI isobmf_box_getmember_length_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI isobmf_box_getmember_ptr_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI isobmf_box_getmember_type_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI isobmf_box_getnextbox_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI isobmf_box_getstate_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI isobmf_box_gettype_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI isobmf_decoder_clear_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI isobmf_decoder_create_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI isobmf_decoder_decode_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI isobmf_decoder_destroy_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI isobmf_decoder_get_decoder_tree_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI isobmf_decoder_getquerydata_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI isobmf_decoder_next_disk_offset_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI isobmf_decoder_query_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI isobmf_memmanager_set_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceBemp2sys/Export.cpp b/core/libs/prx/libSceBemp2sys/Export.cpp index d81f4a5e32..3bf12ad835 100644 --- a/core/libs/prx/libSceBemp2sys/Export.cpp +++ b/core/libs/prx/libSceBemp2sys/Export.cpp @@ -6,5 +6,159 @@ extern "C" { APS5_DUMMY_FUN +int APS5_VABI bemp2sys_logprintf_getlevel_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_logprintf_setfunc_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_logprintf_setlevel_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_memmanager_set_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_packetizer_create_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_packetizer_destroy_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_packetizer_init_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_packetizer_release_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_pesparser_create_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_pesparser_destroy_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_pesparser_init_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_pesparser_parse_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_pesparser_release_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_pesparser_sendmessage_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_psdecoder_create_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_psdecoder_destroy_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_psdecoder_init_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_psdecoder_readpacket_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_psdecoder_release_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_psdecoder_sendmessage_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_tsdecoder_create_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_tsdecoder_destroy_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_tsdecoder_init_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_tsdecoder_readpacket_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_tsdecoder_release_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_tsdecoder_sendmessage_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_tsfilter_create_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_tsfilter_destroy_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_tsfilter_init_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_tsfilter_release_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI bemp2sys_tsfilter_sendmessage_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceBgft/Export.cpp b/core/libs/prx/libSceBgft/Export.cpp index 1a1a66de1b..bb89f27861 100644 --- a/core/libs/prx/libSceBgft/Export.cpp +++ b/core/libs/prx/libSceBgft/Export.cpp @@ -117,5 +117,224 @@ int APS5_VABI sceBgftServiceIntInit() { NotImplemented_nid_no_patch(__func__); return 0; } +int APS5_VABI sceBgftServiceDownloadQueryTask(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntDebugDownloadCorruptPkg(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntDebugDownloadRequest(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntDebugPlayGoGetPlayGoStatusString(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntDebugPlayGoIsPaused(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntDebugPlayGoResume(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntDebugPlayGoRevertToFullState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntDebugPlayGoRevertToInitialState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntDebugPlayGoRevertToSnapshot(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntDebugPlayGoSnapshotByTitleId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntDebugPlayGoSuspend(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntDownloadDebugDeleteBgftEnvFile(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntDownloadDebugDownloadBgftEnvFile(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntDownloadDebugGetBgftEnvInfoString(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntDownloadDebugGetStat(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntDownloadGetExtUsbUserStorageSize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntDownloadGetUserStorageSize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntDownloadPauseTaskAll(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntDownloadQueryTask(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntDownloadRegisterTaskByStorage(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntDownloadRestartTask(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntDownloadResumeTaskAll(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntDownloadStartTaskAll(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntDownloadStopTaskAll(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntDownloadUnregisterTaskAll(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntExtUsbCheckActiveDownloadTasks(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntGetTaskInfoValueInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntGetTaskInfoValueString(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntGetTaskInfoValueStringIndex(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntPlayGoFindTaskId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntPlayGoGetDiscIniChunksInstallProgress(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntPlayGoIsPaused(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntPlayGoResume(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntTerm(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntUploadGetProgress(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntUploadQueryTask(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntUploadRegisterTask(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntUploadRestartTask(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntUploadStartTask(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntUploadStartTaskAll(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntUploadStopTask(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntUploadStopTaskAll(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntUploadUnregisterTask(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceBgftServiceIntUploadUnregisterTaskAll(void) { + 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..5b7a222827 100644 --- a/core/libs/prx/libSceBluetoothHid/Export.cpp +++ b/core/libs/prx/libSceBluetoothHid/Export.cpp @@ -49,4 +49,48 @@ int APS5_VABI sceBluetoothHidSetOutputReport() { return 0; } +int APS5_VABI sceBluetoothHidDebugGetVersion() { + return 0x1e; +} + +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/libSceCdlgG2P/Export.cpp b/core/libs/prx/libSceCdlgG2P/Export.cpp index d772b29003..082b4bb331 100644 --- a/core/libs/prx/libSceCdlgG2P/Export.cpp +++ b/core/libs/prx/libSceCdlgG2P/Export.cpp @@ -59,6 +59,7 @@ int APS5_VABI sceG2PDialogGetResult(void* result) { int APS5_VABI sceG2PDialogClose(void) { if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } diff --git a/core/libs/prx/libSceCdlgPlayerReview/Export.cpp b/core/libs/prx/libSceCdlgPlayerReview/Export.cpp index 7113ee245a..2e454c8741 100644 --- a/core/libs/prx/libSceCdlgPlayerReview/Export.cpp +++ b/core/libs/prx/libSceCdlgPlayerReview/Export.cpp @@ -46,6 +46,7 @@ int APS5_VABI scePlayerReviewDialogOpen(const void* param) { int APS5_VABI scePlayerReviewDialogClose(void) { if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + g_status = COMMON_DIALOG_STATUS_FINISHED; 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..d18971f49b 100644 --- a/core/libs/prx/libSceCesCs/CesLibrary.cmake +++ b/core/libs/prx/libSceCesCs/CesLibrary.cmake @@ -1,5 +1,8 @@ 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 + ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/PendingExports.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 ec650f1b6f..50005b98f8 100644 --- a/core/libs/prx/libSceCesCs/Export.cpp +++ b/core/libs/prx/libSceCesCs/Export.cpp @@ -51,6 +51,12 @@ constexpr uint16_t CP1250_HIGH[128] = { 0x0111, 0x0144, 0x0148, 0x00F3, 0x00F4, 0x0151, 0x00F6, 0x00F7, 0x0159, 0x016F, 0x00FA, 0x0171, 0x00FC, 0x00FD, 0x0163, 0x02D9, }; +constexpr uint16_t CP932_IBM_HEAD[28] = { + 0x2170, 0x2171, 0x2172, 0x2173, 0x2174, 0x2175, 0x2176, 0x2177, 0x2178, 0x2179, + 0x2160, 0x2161, 0x2162, 0x2163, 0x2164, 0x2165, 0x2166, 0x2167, 0x2168, 0x2169, + 0xFFE2, 0xFFE4, 0xFF07, 0xFF02, 0x3231, 0x2116, 0x2121, 0x2235, +}; + uint32_t sbcToUnicode(const uint8_t* profile, uint8_t sbc) { if (profile == CP1251_PROFILE || profile == CP1250_PROFILE) { const uint16_t* high = profile == CP1251_PROFILE ? CP1251_HIGH : CP1250_HIGH; @@ -126,11 +132,16 @@ uint32_t encodeUtf8(uint32_t code, uint8_t* utf8) { } constexpr uint8_t EUC_JP_TAG[8] = {'E', 'U', 'C', 'J', 'P', '9', '3', '2'}; +constexpr uint8_t SJIS_TAG[8] = {'S', 'J', 'I', 'S', '1', '9', '9', '7'}; bool isEucJp(const void* profile) { return profile != nullptr && std::memcmp(profile, EUC_JP_TAG, sizeof(EUC_JP_TAG)) == 0; } +bool isSjis(const void* profile) { + return profile != nullptr && std::memcmp(profile, SJIS_TAG, sizeof(SJIS_TAG)) == 0; +} + bool isSbc(const void* profile) { return profile == CP1252_PROFILE || profile == CP1251_PROFILE || profile == CP1250_PROFILE; } @@ -148,6 +159,56 @@ int decodeMbcs(const void* profile, const uint8_t* mbcs, uint32_t mbcsMax, uint3 code = sbcToUnicode(static_cast(profile), lead); return 0; } + if (isSjis(profile)) { + if (lead < 0x80) { + length = 1; + code = lead; + return 0; + } + if (lead >= 0xA1 && lead <= 0xDF) { + length = 1; + code = 0xFF61 + (lead - 0xA1); + return 0; + } + if ((lead >= 0x81 && lead <= 0x9F) || (lead >= 0xE0 && lead <= 0xFC)) { + if (mbcsMax < 2) return CES_ERROR_SRC_BUFFER_END; + const uint8_t trail = mbcs[1]; + if (trail < 0x40 || trail == 0x7F || trail > 0xFC) return CES_ERROR_INVALID_ENCODE; + if (lead >= 0xFA) { + const uint32_t trailIdx = trail < 0x7F ? trail - 0x40 : trail - 0x41; + const uint32_t offset = (lead - 0xFA) * 188 + trailIdx; + if (offset <= 387) { + if (offset < 28) { + code = CP932_IBM_HEAD[offset]; + length = 2; + return 0; + } + const uint32_t kanjiIdx = offset - 28; + const int row = 89 + kanjiIdx / 94; + const int cell = 1 + kanjiIdx % 94; + code = JisX0208::TO_UCS2[(row - 1) * 94 + (cell - 1)]; + if (code == 0) return CES_ERROR_UNASSIGNED_CODE; + length = 2; + return 0; + } + return CES_ERROR_UNASSIGNED_CODE; + } + int row = (lead <= 0x9F) ? (lead - 0x81) * 2 + 1 : (lead - 0xE0) * 2 + 63; + int cell = 0; + if (trail >= 0x9F) { + row += 1; + cell = trail - 0x9E; + } else { + cell = (trail > 0x7E) ? trail - 0x40 : trail - 0x3F; + } + if (row < 1 || row > 94 || cell < 1 || cell > 94) return CES_ERROR_UNASSIGNED_CODE; + code = JisX0208::TO_UCS2[(row - 1) * 94 + (cell - 1)]; + if (code == 0) return CES_ERROR_UNASSIGNED_CODE; + length = 2; + return 0; + } + return CES_ERROR_INVALID_ENCODE; + } if (lead < 0x80) { length = 1; code = lead; @@ -205,6 +266,33 @@ int encodeMbcs(const void* profile, uint32_t code, uint8_t* mbcs, uint32_t& leng length = 1; return unicodeToSbc(static_cast(profile), code, mbcs[0]) ? 0 : CES_ERROR_UNASSIGNED_CODE; } + if (isSjis(profile)) { + if (code < 0x80) { + length = 1; + mbcs[0] = static_cast(code); + return 0; + } + if (code >= 0xFF61 && code <= 0xFF9F) { + length = 1; + mbcs[0] = static_cast(0xA1 + (code - 0xFF61)); + return 0; + } + if (code > 0xFFFF) return CES_ERROR_UNASSIGNED_CODE; + const auto& reverse = jisReverse(); + const auto found = std::lower_bound(reverse.begin(), reverse.end(), static_cast(code), [](const JisCell& entry, uint16_t value) { return entry.code < value; }); + if (found == reverse.end() || found->code != code || found->supplementary) return CES_ERROR_UNASSIGNED_CODE; + const int row = found->cell / 94 + 1; + const int cell = found->cell % 94 + 1; + if (row % 2 != 0) { + mbcs[0] = static_cast(row < 63 ? 0x81 + (row - 1) / 2 : 0xE0 + (row - 63) / 2); + mbcs[1] = static_cast(cell <= 63 ? cell + 0x3F : cell + 0x40); + } else { + mbcs[0] = static_cast(row < 63 ? 0x81 + (row - 2) / 2 : 0xE0 + (row - 64) / 2); + mbcs[1] = static_cast(cell + 0x9E); + } + length = 2; + return 0; + } if (code < 0x80) { length = 1; mbcs[0] = static_cast(code); @@ -248,7 +336,7 @@ template int toMbcs(const void* context, const Source* source, uint32_t sourceMax, uint32_t* sourceLen, uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen, Decode decode) { if (!context) return CES_ERROR_INVALID_PARAMETER; const void* profile = contextProfile(context); - if (!isEucJp(profile) && !isSbc(profile)) return CES_ERROR_INVALID_PARAMETER; + if (!isEucJp(profile) && !isSjis(profile) && !isSbc(profile)) return CES_ERROR_INVALID_PARAMETER; if (!source) return CES_ERROR_INVALID_SRC_BUFFER; uint32_t read = 0; uint32_t written = 0; @@ -275,7 +363,7 @@ template int fromMbcs(const void* context, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen, Target* target, uint32_t targetMax, uint32_t* targetLen, Encode encode) { if (!context) return CES_ERROR_INVALID_PARAMETER; const void* profile = contextProfile(context); - if (!isEucJp(profile) && !isSbc(profile)) return CES_ERROR_INVALID_PARAMETER; + if (!isEucJp(profile) && !isSjis(profile) && !isSbc(profile)) return CES_ERROR_INVALID_PARAMETER; if (!mbcs) return CES_ERROR_INVALID_SRC_BUFFER; uint32_t read = 0; uint32_t written = 0; @@ -315,6 +403,7 @@ const void* APS5_VABI sceCesRefersUcsProfileCp1250(void) { void* APS5_VABI sceCesUcsProfileInitSJis1997Cp932(void* sheet) { if (!sheet) return nullptr; + std::memcpy(sheet, SJIS_TAG, sizeof(SJIS_TAG)); return sheet; } @@ -405,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/PendingExports.cpp b/core/libs/prx/libSceCesCs/PendingExports.cpp new file mode 100644 index 0000000000..165fe278e8 --- /dev/null +++ b/core/libs/prx/libSceCesCs/PendingExports.cpp @@ -0,0 +1,1250 @@ +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceCesBig5ToUcs2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesBig5ToUtf16(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesBig5ToUtf16be(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesBig5ToUtf16le(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesBig5ToUtf32(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesBig5ToUtf32be(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesBig5ToUtf32le(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesEucCnToUcs2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesEucCnToUtf16(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesEucCnToUtf16be(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesEucCnToUtf16le(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesEucCnToUtf32(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesEucCnToUtf32be(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesEucCnToUtf32le(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesEucCnToUtf8(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesEucCnUcsProfileSetSbcs(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesEucJpToUtf16(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesEucJpToUtf16be(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesEucJpToUtf16le(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesEucJpToUtf32(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesEucJpToUtf32be(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesEucJpToUtf32le(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesEucJpUcsProfileSetSbcs(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesEucKrToUcs2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesEucKrToUtf16(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesEucKrToUtf16be(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesEucKrToUtf16le(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesEucKrToUtf32(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesEucKrToUtf32be(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesEucKrToUtf32le(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesEucKrUcsProfileSetSbcs(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesGbToUcs2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesGbToUtf16(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesGbToUtf16be(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesGbToUtf16le(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesGbToUtf32(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesGbToUtf32be(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesGbToUtf32le(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesGbToUtf8(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesGbUcsProfileSet4ByteCharRange(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesGbUcsProfileSetSbcs(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesGbUcsProfileSetUdaMapping(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesIso2022JpUcsProfileSetSbcs(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesIso2022StrGetUcs2Len(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesIso2022StrGetUtf16Len(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesIso2022StrGetUtf32Len(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesIso2022StrGetUtf8Len(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesIso2022StrToUcs2Str(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesIso2022StrToUtf16Str(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesIso2022StrToUtf32Str(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesIso2022StrToUtf8Str(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesIso2022UcsContextInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesIso2022UcsContextInitCopy(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesJisGetLevel(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesJisX0208ToSJisCode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesJisX0213ToSJisCode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesJiscsToUcs(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesMbcToUcs2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesMbcToUtf16(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesMbcToUtf16be(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesMbcToUtf16le(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesMbcToUtf32(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesMbcToUtf32be(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesMbcToUtf32le(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesMbcToUtf8(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesMbcsStrGetUtf16Len(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesMbcsStrGetUtf32Len(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesMbcsStrToUtf16Str(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesMbcsStrToUtf32Str(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesMbcsUcsContextInitCopy(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesMbcsUcsSetMbcsReplacementCharCode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesMbcsUcsSetMbcsReplacementCharUCode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileAscii(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileAsciiWithKatakana(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileBs4730(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileCp1253(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileCp1254(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileCp1255(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileCp1256(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileCp1257(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileCp1258(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileCp437(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileCp737(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileCp775(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileCp850(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileCp852(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileCp855(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileCp857(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileCp858(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileCp860(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileCp861(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileCp862(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileCp863(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileCp864(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileCp865(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileCp866(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileCp869(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileCp874(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileGbT1988(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileGbT1988Tilde0x7e(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileIso8859_1(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileIso8859_10(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileIso8859_11(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileIso8859_13(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileIso8859_14(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileIso8859_15(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileIso8859_16(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileIso8859_2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileIso8859_3(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileIso8859_4(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileIso8859_5(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileIso8859_6(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileIso8859_7(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileIso8859_8(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileIso8859_9(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileJisX0201(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileJisX0201Katakana(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileJisX0201Roman(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileJisX0201RomanTilde0x7e(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileJisX0201Tilde0x7e(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileKoi8R(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileKoi8U(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileKsX1003(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesRefersUcsProfileKsX1003Tilde0x7e(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesSJisCodeToJisX0208(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesSJisCodeToJisX0213(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesSJisGetCode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesSJisPutCode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesSJisToUcs2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesSJisToUtf16(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesSJisToUtf16be(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesSJisToUtf16le(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesSJisToUtf32(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesSJisToUtf32be(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesSJisToUtf32le(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesSJisToUtf8(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesSJisUcsProfileSetSbcs(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesSbcToUcs2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesSbcToUtf16(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesSbcToUtf16be(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesSbcToUtf16le(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesSbcToUtf32(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesSbcToUtf32be(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesSbcToUtf32le(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUcs2StrGetIso2022Len(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUcs2StrToIso2022Str(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUcs2ToBig5(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUcs2ToEucCn(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUcs2ToEucJp(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUcs2ToEucKr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUcs2ToGb(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUcs2ToMbc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUcs2ToSJis(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUcs2ToSbc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUcs2ToUhc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUcsProfileInitEucCnGb2312(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUcsProfileInitEucJis2004(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUcsProfileInitEucJpX0208(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUcsProfileInitEucJpX0208Ss2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUcsProfileInitEucJpX0208Ss2Ss3(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUcsProfileInitGb18030_2000(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUcsProfileInitGb18030_2005(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUcsProfileInitGbkCp936(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUcsProfileInitIso2022JpCp50221(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUcsProfileInitIso2022JpCp50222(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUcsProfileInitIso2022JpRfc1468(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUcsProfileInitSJis1997X0208(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUcsProfileInitSJis2004X0213(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUcsToJiscs(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUhcToUcs2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUhcToUtf16(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUhcToUtf16be(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUhcToUtf16le(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUhcToUtf32(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUhcToUtf32be(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUhcToUtf32le(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUhcUcsProfileSetSbcs(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16StrGetIso2022Len(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16StrGetMbcsLen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16StrToIso2022Str(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16StrToMbcsStr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16ToBig5(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16ToEucCn(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16ToEucJp(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16ToEucKr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16ToGb(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16ToMbc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16ToSJis(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16ToSbc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16ToUhc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16beToBig5(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16beToEucCn(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16beToEucJp(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16beToEucKr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16beToGb(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16beToMbc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16beToSJis(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16beToSbc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16beToUhc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16leToBig5(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16leToEucCn(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16leToEucJp(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16leToEucKr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16leToGb(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16leToMbc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16leToSJis(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16leToSbc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf16leToUhc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32StrGetIso2022Len(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32StrGetMbcsLen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32StrToIso2022Str(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32StrToMbcsStr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32ToBig5(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32ToEucCn(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32ToEucJp(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32ToEucKr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32ToGb(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32ToMbc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32ToSJis(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32ToSbc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32ToUhc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32beToBig5(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32beToEucCn(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32beToEucJp(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32beToEucKr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32beToGb(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32beToMbc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32beToSJis(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32beToSbc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32beToUhc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32leToBig5(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32leToEucCn(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32leToEucJp(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32leToEucKr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32leToGb(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32leToMbc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32leToSJis(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32leToSbc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf32leToUhc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf8StrGetIso2022Len(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf8StrToIso2022Str(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf8ToBig5(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf8ToEucCn(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf8ToEucJp(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf8ToEucKr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf8ToGb(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf8ToMbc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf8ToSJis(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCesUtf8ToUhc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} 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/libSceCommonDialog/CommonDialog.hpp b/core/libs/prx/libSceCommonDialog/CommonDialog.hpp index 3f6451d34f..c9eb03df55 100644 --- a/core/libs/prx/libSceCommonDialog/CommonDialog.hpp +++ b/core/libs/prx/libSceCommonDialog/CommonDialog.hpp @@ -2,6 +2,6 @@ #define CORE_LIBS_PRX_LIBSCECOMMONDIALOG_COMMONDIALOG_HPP constexpr int COMMON_DIALOG_OK = 0; -constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast(0x80B80002); +constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast(0x80B80004); #endif diff --git a/core/libs/prx/libSceCommonDialog/Export.cpp b/core/libs/prx/libSceCommonDialog/Export.cpp index 8fb415d4eb..bb18755ae2 100644 --- a/core/libs/prx/libSceCommonDialog/Export.cpp +++ b/core/libs/prx/libSceCommonDialog/Export.cpp @@ -1,19 +1,20 @@ +#include #include #include #include "SceTypes.hpp" #include "prx/libSceCommonDialog/CommonDialog.hpp" #include "prx/libc/include/General.hpp" -static bool g_initialized = false; +static std::atomic g_initialized{false}; extern "C" { int APS5_VABI sceCommonDialogInitialize(void) { - if (g_initialized) { - return COMMON_DIALOG_ERROR_ALREADY_INITIALIZED; - } - g_initialized = true; - return COMMON_DIALOG_OK; + bool expected = false; + if (!g_initialized.compare_exchange_strong(expected, true)) { + return COMMON_DIALOG_ERROR_ALREADY_INITIALIZED; + } + return COMMON_DIALOG_OK; } bool APS5_VABI sceCommonDialogIsUsed(void) { 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..7a208122da 100644 --- a/core/libs/prx/libSceCompanionUtil/Export.cpp +++ b/core/libs/prx/libSceCompanionUtil/Export.cpp @@ -5,6 +5,24 @@ extern "C" { -APS5_DUMMY_FUN +int APS5_VABI sceCompanionUtilGetEvent() { + return static_cast(0x80AD0008); +} + +int APS5_VABI sceCompanionUtilGetRemoteOskEvent() { + return 0; +} + +int APS5_VABI sceCompanionUtilInitialize() { + return 0; +} + +int APS5_VABI sceCompanionUtilOptParamInitialize() { + return 0; +} + +int APS5_VABI sceCompanionUtilTerminate() { + 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..2c9b8a5777 100644 --- a/core/libs/prx/libSceContentSearch/Export.cpp +++ b/core/libs/prx/libSceContentSearch/Export.cpp @@ -141,4 +141,45 @@ int APS5_VABI sceContentSearchTerm(void) { return 0; } +int APS5_VABI sceContentBinderEnd() { + return static_cast(0x809D000F); +} +int APS5_VABI sceContentBinderGetNumOfDifference() { + return static_cast(0x809D000F); +} +int APS5_VABI sceContentBinderInitialize() { + return static_cast(0x809D000F); +} +int APS5_VABI sceContentBinderIsUpdated() { + return static_cast(0x809D000F); +} +int APS5_VABI sceContentBinderReset() { + return static_cast(0x809D000F); +} +int APS5_VABI sceContentBinderSearchDifference() { + return static_cast(0x809D000F); +} +int APS5_VABI sceContentBinderStart() { + return static_cast(0x809D000F); +} +int APS5_VABI sceContentBinderTerminate() { + return static_cast(0x809D000F); +} +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/libSceDepth/Export.cpp b/core/libs/prx/libSceDepth/Export.cpp index fb34206c21..edfa2e580e 100644 --- a/core/libs/prx/libSceDepth/Export.cpp +++ b/core/libs/prx/libSceDepth/Export.cpp @@ -6,5 +6,197 @@ extern "C" { APS5_DUMMY_FUN +int APS5_VABI sceDepthCalculateRectificationInformation(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthDisableHeteroMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthEnableExtendedMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthEnableHeteroMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthEnableHeteroModeAndGenerateInformation(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthGetCalibrationData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthGetImage(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthGetRectificationInformation(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthGetSrParameter(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthHandCandidateTrackerGetResult(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthHasCalibrationData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthHeadCandidateDetectorGetResults(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthHeadCandidateTrackerGetResult(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthHeadCandidateTrackerSetValidationInformation(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthInitialize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthLoadCalibrationData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthQueryMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthSaveInternalBuffers(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthSetCalibrationData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthSetCommands(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthSetPostFilterParameter(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthSetPreFilterParameter(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthSetRectificationInformation(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthSetRoi(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthSetRoiset(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthSetSrParameter(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthSetSrTnrParameter(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthSubmit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthTerminate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthUpdate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthValidateCurrentCalibrationData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepthWaitAndExecutePostProcess(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} +extern "C" { + +int APS5_VABI _sceDepthCalcHeteroAdjustParam() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _sceDepthHeadCandidateTrackerGetInformation() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _sceDepthHeadCandidateTrackerSetInformation() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _sceDepthSetCameraFactoryCalibrationData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _sceDepthSetHeteroAdjustParam() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _sceDepthSetHeteroMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceDepth2/Export.cpp b/core/libs/prx/libSceDepth2/Export.cpp new file mode 100644 index 0000000000..d043a5728d --- /dev/null +++ b/core/libs/prx/libSceDepth2/Export.cpp @@ -0,0 +1,58 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceDepth2GetImage(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepth2Initialize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepth2QueryMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepth2SaveInternalBuffers(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepth2SetCameraFactoryCalibrationData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepth2SetCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepth2SetRoi(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepth2Submit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepth2Terminate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceDepth2WaitAndExecutePostProcess(void) { + 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..d3c922f2b1 100644 --- a/core/libs/prx/libSceErrorDialog/Export.cpp +++ b/core/libs/prx/libSceErrorDialog/Export.cpp @@ -21,6 +21,7 @@ constexpr int kErrOk = 0; constexpr int kErrNotInitialized = static_cast(0x80ED0001u); constexpr int kErrAlreadyInitialized = static_cast(0x80ED0002u); constexpr int kErrParam = static_cast(0x80ED0003u); +constexpr int kErrUnexpectedFatal = static_cast(0x80ED0004u); constexpr int kErrInvalidState = static_cast(0x80ED0005u); constexpr std::size_t kParamSize = 16; @@ -92,4 +93,12 @@ int APS5_VABI sceErrorDialogTerminate(void) { return kErrOk; } +int APS5_VABI sceErrorDialogOpenDetail() { + return kErrUnexpectedFatal; +} + +int APS5_VABI sceErrorDialogOpenWithReport() { + return kErrUnexpectedFatal; +} + } diff --git a/core/libs/prx/libSceEsvm/Export.cpp b/core/libs/prx/libSceEsvm/Export.cpp index d81f4a5e32..54def11467 100644 --- a/core/libs/prx/libSceEsvm/Export.cpp +++ b/core/libs/prx/libSceEsvm/Export.cpp @@ -8,3 +8,21 @@ extern "C" { APS5_DUMMY_FUN } +extern "C" { + +int APS5_VABI sceEsvmAppAudioSyncCreateContext() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceEsvmAppAudioSyncFlush() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceEsvmSyncVideo() { + 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/libSceFont/include/FontInternal.hpp b/core/libs/prx/libSceFont/include/FontInternal.hpp index b2a742c291..8c68010f10 100644 --- a/core/libs/prx/libSceFont/include/FontInternal.hpp +++ b/core/libs/prx/libSceFont/include/FontInternal.hpp @@ -24,13 +24,21 @@ constexpr std::uint8_t STYLE_FRAME_FLAG_SLANT = 0x02; constexpr std::uint8_t STYLE_FRAME_FLAG_WEIGHT = 0x04; constexpr float POINTS_PER_INCH = 72.0f; +struct FallbackFace { + std::shared_ptr> data; + FT_Face face = nullptr; + std::uint32_t subFontIndex = 0; +}; + struct FontState { std::shared_ptr> faceData; FT_Face face = nullptr; + std::vector fallbacks; float scaleW = 16.0f; float scaleH = 16.0f; std::map scriptLanguages; std::map typographicFeatures; + int attribute = 0; FontState() = default; FontState(const FontState&) = delete; @@ -52,6 +60,7 @@ struct GeneratedGlyph { std::vector outlineContours; bool metricsInitialized = false; bool outlineInitialized = false; + int attribute = 0; }; struct SystemFontFile { @@ -88,11 +97,15 @@ FontState* TryGetState(FontHandle handle); FontState& ResetState(FontHandle handle); void RemoveState(FontHandle handle); void LoadStateFace(FontState& state, std::shared_ptr> bytes, std::uint32_t subFontIndex); +void AddStateFallbackFace(FontState& state, std::shared_ptr> bytes, std::uint32_t subFontIndex); +FT_Face FallbackFaceForCode(const FontState& state, std::uint32_t code, FT_UInt* glyphIndex); +FT_Face FaceForCode(const FontState& state, std::uint32_t code, FT_UInt* glyphIndex); bool IsSystemFontSet(std::uint32_t fontSetType); std::vector SystemFontCandidates(std::uint32_t fontSetType); std::filesystem::path SystemFontDirectory(); std::optional FindSystemFontFile(std::uint32_t fontSetType); +std::vector FindSystemFontFallbackFiles(std::uint32_t fontSetType); void TrackGeneratedGlyph(FontGlyph glyph); bool ForgetGeneratedGlyph(FontGlyph glyph); diff --git a/core/libs/prx/libSceFont/src/Layout.cpp b/core/libs/prx/libSceFont/src/Layout.cpp index 8a067968b8..a59a3b5f2f 100644 --- a/core/libs/prx/libSceFont/src/Layout.cpp +++ b/core/libs/prx/libSceFont/src/Layout.cpp @@ -404,10 +404,10 @@ int Font::GetCharGlyphMetrics(FontHandle handle, std::uint32_t code, FontGlyphMe float scaleH = 0.0f; const int scaleRc = StyleStateGetScalePixel(style, &scaleW, &scaleH); if (scaleRc != SCE_FONT_OK) return finish(scaleRc); - const FT_Face face = state->face; - if (!face) return finish(SCE_FONT_ERROR_NO_SUPPORT_FUNCTION); - const FT_UInt glyphIndex = ResolveGlyphIndexWithFallback(face, code); - if (glyphIndex == 0) return finish(SCE_FONT_ERROR_NO_SUPPORT_GLYPH); + if (!state->face) return finish(SCE_FONT_ERROR_NO_SUPPORT_FUNCTION); + FT_UInt glyphIndex = 0; + const FT_Face face = FaceForCode(*state, code, &glyphIndex); + if (!face) return finish(SCE_FONT_ERROR_NO_SUPPORT_GLYPH); const auto charW = static_cast(static_cast(scaleW * 64.0f)); const auto charH = static_cast(static_cast(scaleH * 64.0f)); FT_F26Dot6 usedW = charW; @@ -475,19 +475,23 @@ int Font::RenderCharGlyphImageCore(FontHandle handle, std::uint32_t code, FontRe obj->shift_units_y = 0; obj->layout_seed_pair = 0; StyleStateGetScalePixel(style, &obj->scale_x_0x50, &obj->scale_y_0x54); - float outScaleX = 0.0f; - float outScaleY = 0.0f; - int rc; - if (style->scale_unit == 0) { - rc = driver->set_char_default_dpi(obj, style->scale_w, style->scale_h, &outScaleX, &outScaleY); - } else { - rc = driver->set_char_with_dpi(obj, style->dpi_x ? style->dpi_x : 0x48, style->dpi_y ? style->dpi_y : 0x48, style->scale_w, style->scale_h, &outScaleX, &outScaleY); - } - if (rc != SCE_FONT_OK) return finish(SCE_FONT_ERROR_NO_SUPPORT_GLYPH); + const auto setCharSize = [&](FontObj* target) { + float outScaleX = 0.0f; + float outScaleY = 0.0f; + if (style->scale_unit == 0) return driver->set_char_default_dpi(target, style->scale_w, style->scale_h, &outScaleX, &outScaleY); + return driver->set_char_with_dpi(target, style->dpi_x ? style->dpi_x : 0x48, style->dpi_y ? style->dpi_y : 0x48, style->scale_w, style->scale_h, &outScaleX, &outScaleY); + }; + if (setCharSize(obj) != SCE_FONT_OK) return finish(SCE_FONT_ERROR_NO_SUPPORT_GLYPH); std::uint32_t glyphIndex = 0; - rc = driver->glyph_index(obj, code, &glyphIndex); - if (rc != SCE_FONT_OK || glyphIndex == 0) return finish(SCE_FONT_ERROR_NO_SUPPORT_GLYPH); - return finish(RenderGlyphIndexToSurface(*obj, glyphIndex, surface, x, y, metrics, result)); + if (driver->glyph_index(obj, code, &glyphIndex) == SCE_FONT_OK && glyphIndex != 0) return finish(RenderGlyphIndexToSurface(*obj, glyphIndex, surface, x, y, metrics, result)); + const FontState* state = TryGetState(handle); + FT_UInt fallbackIndex = 0; + const FT_Face fallbackFace = state ? FallbackFaceForCode(*state, code, &fallbackIndex) : nullptr; + if (!fallbackFace) return finish(SCE_FONT_ERROR_NO_SUPPORT_GLYPH); + FontObj fallback = *obj; + fallback.ft_face = fallbackFace; + if (setCharSize(&fallback) != SCE_FONT_OK) return finish(SCE_FONT_ERROR_NO_SUPPORT_GLYPH); + return finish(RenderGlyphIndexToSurface(fallback, fallbackIndex, surface, x, y, metrics, result)); } bool Font::BuildTrueOutline(GeneratedGlyph& glyph) { @@ -495,9 +499,9 @@ bool Font::BuildTrueOutline(GeneratedGlyph& glyph) { if (!state || !state->face) return false; const auto* font = GetNativeFont(glyph.owner); if (font->magic != HANDLE_MAGIC) return false; - const FT_Face face = state->face; - const FT_UInt glyphIndex = FT_Get_Char_Index(face, glyph.codepoint); - if (glyphIndex == 0) return false; + FT_UInt glyphIndex = 0; + const FT_Face face = FaceForCode(*state, glyph.codepoint, &glyphIndex); + if (!face) return false; const auto charW = static_cast(static_cast(glyph.glyph.scale_x * 64.0f)); const auto charH = static_cast(static_cast(glyph.glyph.base_scale * 64.0f)); if (FT_Set_Char_Size(face, charW, charH, 72, 72) != 0) return false; diff --git a/core/libs/prx/libSceFont/src/Open.cpp b/core/libs/prx/libSceFont/src/Open.cpp index ef118bd83e..21e0272990 100644 --- a/core/libs/prx/libSceFont/src/Open.cpp +++ b/core/libs/prx/libSceFont/src/Open.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include +#include #include #include #include @@ -227,6 +228,13 @@ std::shared_ptr> SystemFontBytes(const std::fil return bytes; } +void WarnMissingSystemFont(std::uint32_t fontSetType) { + static std::once_flag warned; + std::call_once(warned, [&] { + std::fprintf(stderr, "[libSceFont] no file for system font set 0x%08X in %s. Titles that lay out text in system fonts can fail far from here without them: put the console's fonts or the Noto substitutes from docs/user/USAGE.md there, or set ANYPS5_SYSTEM_FONTS to their directory.\n", fontSetType, SystemFontDirectory().string().c_str()); + }); +} + void CopyStyleToCache(FontHandleNative* font) { std::memcpy(&font->cached_style.state, &font->style, sizeof(StyleStateBlock)); } @@ -392,6 +400,7 @@ int APS5_VABI sceFontOpenFontInstance(FontHandle fontHandle, FontHandle setupFon targetState.scaleW = sourceState->scaleW; targetState.scaleH = sourceState->scaleH; if (sourceState->faceData && !sourceState->faceData->empty()) LoadStateFace(targetState, sourceState->faceData, subFontIndex); + for (const FallbackFace& fallback : sourceState->fallbacks) AddStateFallbackFace(targetState, fallback.data, fallback.subFontIndex); } if (pFontHandle) *pFontHandle = target; return SCE_FONT_OK; @@ -407,6 +416,7 @@ int APS5_VABI sceFontOpenFontSet(FontLibrary library, std::uint32_t fontSetType, if (!pFontHandle) return SCE_FONT_ERROR_INVALID_PARAMETER; const std::optional file = FindSystemFontFile(fontSetType); APS5_LOG_OUT("fontSetType=0x%08X file=%s", fontSetType, file ? file->path.string().c_str() : "(none)"); + if (!file && IsSystemFontSet(fontSetType)) WarnMissingSystemFont(fontSetType); std::uint32_t libraryLock = 0; if (!AcquireLibraryLock(lib, libraryLock)) { *pFontHandle = nullptr; @@ -432,8 +442,13 @@ int APS5_VABI sceFontOpenFontSet(FontLibrary library, std::uint32_t fontSetType, if (openRc == SCE_FONT_ERROR_FS_OPEN_FAILED || openRc == SCE_FONT_ERROR_NO_SUPPORT_FORMAT) return SCE_FONT_ERROR_FONT_OPEN_FAILED; if (openRc != SCE_FONT_OK) return openRc; GetNativeFont(*pFontHandle)->open_info.fontset_flags = fontSetType; + const std::vector fallbackFiles = FindSystemFontFallbackFiles(fontSetType); + for (const SystemFontFile& fallback : fallbackFiles) APS5_LOG_OUT("fontSetType=0x%08X fallback=%s", fontSetType, fallback.path.string().c_str()); FontState& state = ResetState(*pFontHandle); if (auto bytes = SystemFontBytes(file->path)) LoadStateFace(state, std::move(bytes), file->subFontIndex); + for (const SystemFontFile& fallback : fallbackFiles) { + if (auto bytes = SystemFontBytes(fallback.path)) AddStateFallbackFace(state, std::move(bytes), fallback.subFontIndex); + } return SCE_FONT_OK; } ReleaseLibraryLock(lib, libraryLock); diff --git a/core/libs/prx/libSceFont/src/Render.cpp b/core/libs/prx/libSceFont/src/Render.cpp index 55d3de834d..3f78fa635a 100644 --- a/core/libs/prx/libSceFont/src/Render.cpp +++ b/core/libs/prx/libSceFont/src/Render.cpp @@ -81,10 +81,11 @@ int CachedColumnOffset(FontHandle handle, FontHandleNative* font, float& offset) return rc; } -void LoadKerning(FT_Face face, float scaleW, float scaleH, std::uint32_t preCode, std::uint32_t code, FontKerning* kerning) { - const FT_UInt previousGlyph = face ? FT_Get_Char_Index(face, preCode) : 0; - const FT_UInt glyph = face ? FT_Get_Char_Index(face, code) : 0; - if (!face || previousGlyph == 0 || glyph == 0) return; +void LoadKerning(const FontState& state, float scaleW, float scaleH, std::uint32_t preCode, std::uint32_t code, FontKerning* kerning) { + FT_UInt previousGlyph = 0; + FT_UInt glyph = 0; + const FT_Face face = FaceForCode(state, preCode, &previousGlyph); + if (!face || FaceForCode(state, code, &glyph) != face) return; const auto charW = static_cast(static_cast(scaleW * 64.0f)); const auto charH = static_cast(static_cast(scaleH * 64.0f)); FT_Set_Char_Size(face, charW, charH, 72, 72); @@ -187,7 +188,7 @@ int APS5_VABI sceFontGetKerning(FontHandle fontHandle, std::uint32_t preCode, st if (!font || font->magic != HANDLE_MAGIC || !AcquireFontLock(font, fontLock)) return SCE_FONT_ERROR_INVALID_FONT_HANDLE; *kerning = {}; const FontState* state = TryGetState(fontHandle); - if (state) LoadKerning(state->face, state->scaleW, state->scaleH, preCode, code, kerning); + if (state) LoadKerning(*state, state->scaleW, state->scaleH, preCode, code, kerning); ReleaseFontLock(font, fontLock); return SCE_FONT_OK; } @@ -209,7 +210,7 @@ int APS5_VABI sceFontGetRenderScaledKerning(FontHandle fontHandle, std::uint32_t float scaleH = 0.0f; rc = StyleStateGetScalePixel(&font->cached_style.state, &scaleW, &scaleH); const FontState* state = TryGetState(fontHandle); - if (rc == SCE_FONT_OK && state) LoadKerning(state->face, scaleW, scaleH, preCode, code, kerning); + if (rc == SCE_FONT_OK && state) LoadKerning(*state, scaleW, scaleH, preCode, code, kerning); } ReleaseCachedStyleLock(font, cachedLock); ReleaseFontLock(font, fontLock); @@ -419,10 +420,20 @@ int APS5_VABI sceFontDeleteGlyph(const FontMemory* memory, FontGlyph* pGlyph) { return SCE_FONT_OK; } -int APS5_VABI sceFontGlyphDefineAttribute(FontGlyph glyph, std::uint32_t attribute, std::uint64_t value) { +int APS5_VABI sceFontGlyphDefineAttribute(FontGlyph glyph, int attribute, int* oldAttribute) { + auto* generated = TryGetGeneratedGlyph(glyph); + if (!generated) return SCE_FONT_ERROR_INVALID_GLYPH; + if (oldAttribute) *oldAttribute = generated->attribute; + generated->attribute = attribute; + return SCE_FONT_OK; +} + +int APS5_VABI sceFontGlyphGetAttribute(FontGlyph glyph, int attribute, int* nowAttribute) { (void)attribute; - (void)value; - if (!glyph || glyph->magic != GLYPH_MAGIC) return SCE_FONT_ERROR_INVALID_GLYPH; + if (!nowAttribute) return SCE_FONT_ERROR_INVALID_PARAMETER; + auto* generated = TryGetGeneratedGlyph(glyph); + if (!generated) return SCE_FONT_ERROR_INVALID_GLYPH; + *nowAttribute = generated->attribute; return SCE_FONT_OK; } diff --git a/core/libs/prx/libSceFont/src/State.cpp b/core/libs/prx/libSceFont/src/State.cpp index ee86c2dac0..4ebd92348b 100644 --- a/core/libs/prx/libSceFont/src/State.cpp +++ b/core/libs/prx/libSceFont/src/State.cpp @@ -13,6 +13,7 @@ #include #include "prx/libc/include/PreciseWait.hpp" +#include "prx/libSceFont/include/FontFreeType.hpp" #include "prx/libSceFont/include/FontInternal.hpp" namespace { @@ -54,10 +55,9 @@ void ReleaseWord(std::uint32_t& word, std::uint32_t previous) { } Font::FontState::~FontState() { - if (face) { - std::lock_guard lock(faceMutex); - FT_Done_Face(face); - } + std::lock_guard lock(faceMutex); + if (face) FT_Done_Face(face); + for (const FallbackFace& fallback : fallbacks) FT_Done_Face(fallback.face); } void Font::Backoff() { @@ -267,6 +267,36 @@ void Font::LoadStateFace(FontState& state, std::shared_ptr> bytes, std::uint32_t subFontIndex) { + std::lock_guard lock(faceMutex); + const FT_Library library = HostFreeTypeLibrary(); + if (!library || !bytes || bytes->empty()) return; + FT_Face face = nullptr; + if (FT_New_Memory_Face(library, bytes->data(), static_cast(bytes->size()), static_cast(subFontIndex), &face) != 0) return; + FT_Select_Charmap(face, FT_ENCODING_UNICODE); + state.fallbacks.push_back({std::move(bytes), face, subFontIndex}); +} + +FT_Face Font::FallbackFaceForCode(const FontState& state, std::uint32_t code, FT_UInt* glyphIndex) { + for (const FallbackFace& fallback : state.fallbacks) { + const FT_UInt index = ResolveGlyphIndexWithFallback(fallback.face, code); + if (index == 0) continue; + *glyphIndex = index; + return fallback.face; + } + *glyphIndex = 0; + return nullptr; +} + +FT_Face Font::FaceForCode(const FontState& state, std::uint32_t code, FT_UInt* glyphIndex) { + const FT_UInt index = ResolveGlyphIndexWithFallback(state.face, code); + if (index != 0) { + *glyphIndex = index; + return state.face; + } + return FallbackFaceForCode(state, code, glyphIndex); +} + void Font::TrackGeneratedGlyph(FontGlyph glyph) { std::lock_guard lock(glyphMutex); generatedGlyphs.insert(glyph); diff --git a/core/libs/prx/libSceFont/src/Style.cpp b/core/libs/prx/libSceFont/src/Style.cpp index 67e7abe139..e4adb89793 100644 --- a/core/libs/prx/libSceFont/src/Style.cpp +++ b/core/libs/prx/libSceFont/src/Style.cpp @@ -101,6 +101,23 @@ float ClampWeightDelta(float scale) { extern "C" { +int APS5_VABI sceFontDefineAttribute(FontHandle fontHandle, int attribute, int* oldAttribute) { + return AccessFontSettings(fontHandle, [&](FontState& state) { + if (oldAttribute) *oldAttribute = state.attribute; + state.attribute = attribute; + return SCE_FONT_OK; + }); +} + +int APS5_VABI sceFontGetAttribute(FontHandle fontHandle, int attribute, int* nowAttribute) { + (void)attribute; + if (!nowAttribute) return SCE_FONT_ERROR_INVALID_PARAMETER; + return AccessFontSettings(fontHandle, [&](FontState& state) { + *nowAttribute = state.attribute; + return SCE_FONT_OK; + }); +} + int APS5_VABI sceFontSetScriptLanguage(FontHandle fontHandle, int fontScript, int fontLanguage) { return SetFontSetting(fontHandle, &FontState::scriptLanguages, fontScript, fontLanguage); } diff --git a/core/libs/prx/libSceFont/src/SystemFont.cpp b/core/libs/prx/libSceFont/src/SystemFont.cpp index f164d86462..5bcfb0789e 100644 --- a/core/libs/prx/libSceFont/src/SystemFont.cpp +++ b/core/libs/prx/libSceFont/src/SystemFont.cpp @@ -37,7 +37,7 @@ constexpr std::array SYSTEM_FONT_SET_TYPES = { 0x1A0825C3u, 0x1A0825C7u, 0x1A083553u, 0x1A083557u, 0x1A0835D3u, 0x1A0835D7u, }; -enum class Family { Latin, LatinItalic, Typewriter, Vietnamese, Thai, Japanese, ChineseGb }; +enum class Family { Latin, LatinItalic, Typewriter, Vietnamese, Thai, Japanese, ChineseGb, Hangul, Arabic }; struct FamilyFiles { std::array system; @@ -45,7 +45,7 @@ struct FamilyFiles { std::uint32_t substituteSubFont; }; -constexpr std::array FAMILY_FILES = {{ +constexpr std::array FAMILY_FILES = {{ {{"SST-Light.otf", "SST-Roman.otf", "SST-Medium.otf", "SST-Bold.otf"}, {"NotoSans-Light.ttf", "NotoSans-Regular.ttf", "NotoSans-Medium.ttf", "NotoSans-Bold.ttf"}, 0}, {{"SST-LightItalic.otf", "SST-Italic.otf", "SST-MediumItalic.otf", "SST-BoldItalic.otf"}, @@ -60,6 +60,10 @@ constexpr std::array FAMILY_FILES = {{ {"NotoSansCJK-Light.ttc", "NotoSansCJK-Regular.ttc", "NotoSansCJK-Medium.ttc", "NotoSansCJK-Bold.ttc"}, 0}, {{"DFHEI5-SONY.ttf", "DFHEI5-SONY.ttf", "DFHEI5-SONY.ttf", "DFHEI5-SONY.ttf"}, {"NotoSansCJK-Light.ttc", "NotoSansCJK-Regular.ttc", "NotoSansCJK-Medium.ttc", "NotoSansCJK-Bold.ttc"}, 2}, + {{"SCEPS4Yoongd-Medium.otf", "SCEPS4Yoongd-Medium.otf", "SCEPS4Yoongd-Medium.otf", "SCEPS4Yoongd-Bold.otf"}, + {"NotoSansCJK-Light.ttc", "NotoSansCJK-Regular.ttc", "NotoSansCJK-Medium.ttc", "NotoSansCJK-Bold.ttc"}, 1}, + {{"SSTArabic-Light.otf", "SSTArabic-Roman.otf", "SSTArabic-Medium.otf", "SSTArabic-Bold.otf"}, + {"NotoSansArabic-Light.ttf", "NotoSansArabic-Regular.ttf", "NotoSansArabic-Medium.ttf", "NotoSansArabic-Bold.ttf"}, 0}, }}; Family FamilyOf(std::uint32_t fontSetType) { @@ -87,6 +91,37 @@ std::size_t WeightOf(std::uint32_t fontSetType) { } } +std::vector FallbackFamilies(std::uint32_t fontSetType) { + const std::uint32_t scripts = (fontSetType >> 8) & 0xFFu; + const Family primary = FamilyOf(fontSetType); + std::vector families; + const auto add = [&](bool wanted, Family family) { + if (wanted && family != primary && std::find(families.begin(), families.end(), family) == families.end()) families.push_back(family); + }; + add((fontSetType & 0x10u) != 0, Family::Vietnamese); + add((scripts & 0x10u) != 0, Family::Thai); + add((scripts & 0x88u) == 0x88u, Family::ChineseGb); + add((scripts & 0x04u) != 0, Family::Japanese); + add((scripts & 0x80u) != 0, Family::ChineseGb); + add((scripts & 0x20u) != 0, Family::Hangul); + add((fontSetType & 0x80u) != 0, Family::Arabic); + return families; +} + +std::vector FamilyCandidates(Family family, std::size_t weight) { + const FamilyFiles& files = FAMILY_FILES[static_cast(family)]; + return {{std::filesystem::path(files.system[weight]), 0}, {std::filesystem::path(files.substitute[weight]), files.substituteSubFont}}; +} + +std::optional FirstExisting(const std::filesystem::path& directory, const std::vector& candidates) { + for (const Font::SystemFontFile& candidate : candidates) { + std::error_code error; + const std::filesystem::path path = directory / candidate.path; + if (std::filesystem::is_regular_file(path, error)) return Font::SystemFontFile{path, candidate.subFontIndex}; + } + return std::nullopt; +} + std::filesystem::path ExecutableDirectory() { #ifdef _WIN32 wchar_t module[MAX_PATH]; @@ -115,9 +150,7 @@ bool Font::IsSystemFontSet(std::uint32_t fontSetType) { std::vector Font::SystemFontCandidates(std::uint32_t fontSetType) { if (!IsSystemFontSet(fontSetType)) return {}; - const FamilyFiles& files = FAMILY_FILES[static_cast(FamilyOf(fontSetType))]; - const std::size_t weight = WeightOf(fontSetType); - return {{std::filesystem::path(files.system[weight]), 0}, {std::filesystem::path(files.substitute[weight]), files.substituteSubFont}}; + return FamilyCandidates(FamilyOf(fontSetType), WeightOf(fontSetType)); } std::filesystem::path Font::SystemFontDirectory() { @@ -132,11 +165,22 @@ std::filesystem::path Font::SystemFontDirectory() { std::optional Font::FindSystemFontFile(std::uint32_t fontSetType) { const std::vector candidates = SystemFontCandidates(fontSetType); if (candidates.empty()) return std::nullopt; + return FirstExisting(SystemFontDirectory(), candidates); +} + +std::vector Font::FindSystemFontFallbackFiles(std::uint32_t fontSetType) { + if (!IsSystemFontSet(fontSetType)) return {}; const std::filesystem::path directory = SystemFontDirectory(); - for (const SystemFontFile& candidate : candidates) { - std::error_code error; - const std::filesystem::path path = directory / candidate.path; - if (std::filesystem::is_regular_file(path, error)) return SystemFontFile{path, candidate.subFontIndex}; + const std::size_t weight = WeightOf(fontSetType); + std::vector files; + const std::optional primary = FirstExisting(directory, SystemFontCandidates(fontSetType)); + const auto opened = [&](const SystemFontFile& file) { + const auto same = [&](const SystemFontFile& other) { return other.path == file.path && other.subFontIndex == file.subFontIndex; }; + return (primary && same(*primary)) || std::any_of(files.begin(), files.end(), same); + }; + for (const Family family : FallbackFamilies(fontSetType)) { + auto file = FirstExisting(directory, FamilyCandidates(family, weight)); + if (file && !opened(*file)) files.push_back(std::move(*file)); } - return std::nullopt; + return files; } diff --git a/core/libs/prx/libSceFont/src/Unimplemented.cpp b/core/libs/prx/libSceFont/src/Unimplemented.cpp index aa341a63b5..bc79aed66e 100644 --- a/core/libs/prx/libSceFont/src/Unimplemented.cpp +++ b/core/libs/prx/libSceFont/src/Unimplemented.cpp @@ -32,11 +32,6 @@ int APS5_VABI sceFontCreateWords() { return 0; } -int APS5_VABI sceFontDefineAttribute() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceFontDestroyGraphicsDevice() { NotImplemented_nid_no_patch(__func__); return 0; @@ -53,11 +48,6 @@ int APS5_VABI sceFontDestroyWords() { } -int APS5_VABI sceFontGetAttribute() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceFontGetFontGlyphsOutlineProfile() { NotImplemented_nid_no_patch(__func__); return 0; @@ -78,11 +68,6 @@ int APS5_VABI sceFontGetGlyphExpandBufferState() { return 0; } -int APS5_VABI sceFontGlyphGetAttribute() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceFontGlyphRenderImage() { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceFreeType/CMakeLists.txt b/core/libs/prx/libSceFreeType/CMakeLists.txt index 6dcf88f9d3..e0a3b3e116 100644 --- a/core/libs/prx/libSceFreeType/CMakeLists.txt +++ b/core/libs/prx/libSceFreeType/CMakeLists.txt @@ -15,8 +15,15 @@ if(BUILD_TESTING) target_link_libraries(guest_freetype_tests PRIVATE libSceFreeType freetype libc) configure_windows_unwind(guest_freetype_tests) add_test(NAME guest_freetype COMMAND guest_freetype_tests) + add_test_executable(guest_freetype_charmap_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeTypeCharmap.cpp) + target_include_directories(guest_freetype_charmap_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_freetype_charmap_tests PRIVATE libSceFreeType freetype libc) + configure_windows_unwind(guest_freetype_charmap_tests) + add_test(NAME guest_freetype_charmap COMMAND guest_freetype_charmap_tests) if(WIN32) set_tests_properties(guest_freetype PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(guest_freetype_charmap PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceFreeType/Export.cpp b/core/libs/prx/libSceFreeType/Export.cpp index 0d60ccc6f0..0a098e6024 100644 --- a/core/libs/prx/libSceFreeType/Export.cpp +++ b/core/libs/prx/libSceFreeType/Export.cpp @@ -111,4 +111,33 @@ int APS5_VABI FT_Render_Glyph_nid_postfix(GuestGlyphSlotRec* slot, std::int32_t return error; } +int APS5_VABI FT_Select_Charmap_nid_postfix(GuestFaceRec* face, std::int32_t encoding) { + GuestFace* owner = FaceOf(face); + const FT_Error error = FT_Select_Charmap(owner ? owner->host : nullptr, static_cast(encoding)); + if (error == 0) owner->rec.charmap = owner->charmapPointers.at(FT_Get_Charmap_Index(owner->host->charmap)); + return error; +} + +int APS5_VABI FT_Set_Charmap_nid_postfix(GuestFaceRec* face, GuestCharMapRec* charmap) { + GuestFace* owner = FaceOf(face); + if (!owner || !charmap || owner->charmapPointers.empty()) return FT_Set_Charmap(owner ? owner->host : nullptr, nullptr); + for (std::size_t index = 0; index < owner->charmapPointers.size(); ++index) { + if (owner->charmapPointers[index] != charmap) continue; + const FT_Error error = FT_Set_Charmap(owner->host, owner->host->charmaps[index]); + if (error == 0) owner->rec.charmap = charmap; + return error; + } + return FT_Err_Invalid_Argument; +} + +std::uint64_t APS5_VABI FT_Get_First_Char_nid_postfix(GuestFaceRec* face, std::uint32_t* glyphIndex) { + const GuestFace* owner = FaceOf(face); + return FT_Get_First_Char(owner ? owner->host : nullptr, glyphIndex); +} + +std::uint64_t APS5_VABI FT_Get_Next_Char_nid_postfix(GuestFaceRec* face, std::uint64_t charcode, std::uint32_t* glyphIndex) { + const GuestFace* owner = FaceOf(face); + return FT_Get_Next_Char(owner ? owner->host : nullptr, static_cast(static_cast(charcode)), glyphIndex); +} + } diff --git a/core/libs/prx/libSceGameLiveStreaming/Export.cpp b/core/libs/prx/libSceGameLiveStreaming/Export.cpp index b28b535c8e..aa280dcad4 100644 --- a/core/libs/prx/libSceGameLiveStreaming/Export.cpp +++ b/core/libs/prx/libSceGameLiveStreaming/Export.cpp @@ -40,4 +40,9 @@ int APS5_VABI sceGameLiveStreamingGetProgramInfo() { return 0; } +int APS5_VABI sceGameLiveStreamingEnableLiveStreaming(void) { + 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/libSceGnmDriverForNeoMode/Export.cpp b/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp new file mode 100644 index 0000000000..f8a8c67c6a --- /dev/null +++ b/core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp @@ -0,0 +1,598 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceGnmBeginWorkload(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmComputeWaitOnAddress(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmComputeWaitSemaphore(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmCreateWorkloadStream(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDebugModuleReset(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDebugReset(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDestroyWorkloadStream(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDingDongForWorkload(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDispatchDirect(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDispatchIndirect(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDispatchIndirectOnMec(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDrawIndex(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDrawIndexAuto(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDrawIndexIndirect(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDrawIndexIndirectCountMulti(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDrawIndexIndirectMulti(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDrawIndexMultiInstanced(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDrawIndexOffset(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDrawIndirect(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDrawIndirectCountMulti(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDrawIndirectMulti(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDrawInitDefaultHardwareState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDrawInitDefaultHardwareState200(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDrawInitDefaultHardwareState350(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDrawInitToDefaultContextState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDrawInitToDefaultContextState400(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDrawInitToDefaultContextStateInternalCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDrawInitToDefaultContextStateInternalSize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDrawOpaqueAuto(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDriverInternalRetrieveGnmInterface(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDriverInternalRetrieveGnmInterfaceForGpuDebugger(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDriverInternalRetrieveGnmInterfaceForReplay(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDriverInternalRetrieveGnmInterfaceForResourceRegistration(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDriverInternalRetrieveGnmInterfaceForValidation(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDriverInternalVirtualQuery(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmDriverTraceInProgress(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmEndWorkload(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmFindResources(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmFindResourcesPublic(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmGetCoredumpAddress(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmGetCoredumpMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmGetCoredumpProtectionFaultTimestamp(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmGetDbgGcHandle(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmGetGpuBlockStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmGetGpuCoreClockFrequency(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmGetGpuInfoStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmGetLastWaitedAddress(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmGetNumTcaUnits(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmGetOffChipTessellationBufferSize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmGetOwnerName(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmGetPhysicalCounterFromVirtualized(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmGetProtectionFaultTimeStamp(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmGetResourceBaseAddressAndSizeInBytes(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmGetResourceName(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmGetResourceRegistrationBuffers(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmGetResourceShaderGuid(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmGetResourceType(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmGetResourceUserData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmGetShaderProgramBaseAddress(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmGetShaderStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmGpuPaDebugEnter(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmGpuPaDebugLeave(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmInsertDingDongMarker(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmInsertThreadTraceMarker(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmIsCoredumpValid(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmLogicalTcaUnitToPhysical(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmMapComputeQueueWithPriority(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmPaDisableFlipCallbacks(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmPaEnableFlipCallbacks(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmPaHeartbeat(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmQueryResourceRegistrationUserMemoryRequirements(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmRaiseUserExceptionEvent(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmRegisterGdsResource(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmRegisterGnmLiveCallbackConfig(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmRegisterOwner(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmRegisterOwnerForSystem(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmRegisterResource(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmRequestFlipAndSubmitDoneForWorkload(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmResetVgtControl(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmSdmaClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmSdmaConstFill(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmSdmaCopyLinear(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmSdmaCopyTiled(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmSdmaCopyWindow(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmSdmaFlush(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmSdmaGetMinCmdSize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmSdmaOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmSetCsShaderWithModifier(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmSetEmbeddedPsShader(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmSetEmbeddedVsShader(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmSetPsShader350(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmSetResourceRegistrationUserMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmSetResourceUserData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmSetSpiEnableSqCounters(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmSetSpiEnableSqCountersForUnitInstance(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmSetVgtControl(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmSetWaveLimitMultiplier(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmSetWaveLimitMultipliers(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmSubmitAndFlipCommandBuffersForWorkload(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmSubmitCommandBuffersForWorkload(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmUnregisterAllResourcesForOwner(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmUnregisterOwnerAndResources(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmUnregisterResource(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmUpdateGsShader(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmUpdateHsShader(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmUpdatePsShader(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmUpdatePsShader350(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmUpdateVsShader(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmValidateDisableDiagnostics(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmValidateDisableDiagnostics2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmValidateDispatchCommandBuffers(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmValidateDrawCommandBuffers(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmValidateGetDiagnosticInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmValidateGetDiagnostics(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmValidateGetVersion(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmValidateOnSubmitEnabled(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmValidateResetState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGnmValidationRegisterMemoryCheckCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceHmd/Export.cpp b/core/libs/prx/libSceHmd/Export.cpp index 36c2ca3c06..7c1e7c68c7 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,796 @@ 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) { + return 0x400; +} + +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) { + return 0x20000; +} + +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) { + return static_cast(0x81110009); +} + +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..f6f53d804c 100644 --- a/core/libs/prx/libSceHmd2/Export.cpp +++ b/core/libs/prx/libSceHmd2/Export.cpp @@ -1,4 +1,5 @@ #include "Hmd2.hpp" +#include "prx/libc/include/general/ExportMacros.hpp" #include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/General.hpp" #include "prx/libSceHmd/Hmd.hpp" @@ -18,6 +19,12 @@ std::mutex stateMutex; bool initialized = false; std::int32_t openHandle = 0; +std::int32_t BeforeInitialize(std::int32_t answer, const char* name) { + std::lock_guard lock(stateMutex); + if (initialized) NotImplemented_nid_no_patch(name); + return answer; +} + std::uint32_t CompiledSdkVersion() { static const auto* processParameters = static_cast(ApplicationProcessParameters_nid_no_patch()); std::uint32_t version; @@ -48,9 +55,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 +155,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 +178,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() { @@ -221,86 +224,6 @@ std::int32_t APS5_VABI sceHmd2SetVibration(std::int32_t handle, std::uint32_t le return 0; } -int APS5_VABI sceHmd2GazeGetCalibrationData() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2GazeGetCalibrationDataMaxSize() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2ReprojectionClearUserEventEnd() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2ReprojectionClearUserEventStart() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2ReprojectionGetCurrentOutput() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2ReprojectionSetUserEventEnd() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2ReprojectionSetUserEventStart() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2GazeActivateGazeTracking() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2GazeGetDeviceStatus() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2GazeGetUserCalibrationStatus() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2GazeInsertRawDataWithHandle() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2GazeSetEnabledEye() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2GazeSetUserCalibrationData() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2GazeSetUserCalibrationPoint() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2GazeStartUserCalibration() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2GazeStopUserCalibration() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceHmd2ReprojectionSeeThroughDraw() { NotImplemented_nid_no_patch(__func__); return 0; @@ -326,26 +249,6 @@ int APS5_VABI sceHmd2SeeThroughCallbackGetPoseDataFunc() { return 0; } -int APS5_VABI sceHmd2GazeObjectSelectionGetSelectedObject() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2GazeObjectSelectionInitialize() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2GazeObjectSelectionQueryMemory() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2GazeObjectSelectionTerminate() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceHmd2ReprojectionClear() { NotImplemented_nid_no_patch(__func__); return 0; @@ -371,14 +274,546 @@ int APS5_VABI sceHmd2RpeorjectionSetParam() { return 0; } -int APS5_VABI sceHmd2SetFieldOfView() { + +std::int32_t APS5_VABI Hmd2BeforeInitializeZero() { + return BeforeInitialize(0, __func__); +} +APS5_EXPORT("vDgpdS2N9Rw", Hmd2BeforeInitializeZero); +APS5_EXPORT("Ri35gKTZpvE", Hmd2BeforeInitializeZero); +APS5_EXPORT("9oyW8puiKeI", Hmd2BeforeInitializeZero); +APS5_EXPORT("1HZNE6jhBnQ", Hmd2BeforeInitializeZero); +APS5_EXPORT("cdwAKBZLXjs", Hmd2BeforeInitializeZero); +APS5_EXPORT("Yub1MF58jfY", Hmd2BeforeInitializeZero); +APS5_EXPORT("JYXAuQFnPyw", Hmd2BeforeInitializeZero); +APS5_EXPORT("VuPKqCu3wzw", Hmd2BeforeInitializeZero); +APS5_EXPORT("GSThagVyW5U", Hmd2BeforeInitializeZero); +APS5_EXPORT("J4qE5N937MY", Hmd2BeforeInitializeZero); +APS5_EXPORT("9KnNj-dyASg", Hmd2BeforeInitializeZero); +APS5_EXPORT("Qta2bkstbFk", Hmd2BeforeInitializeZero); +APS5_EXPORT("5ZcObnDRBXE", Hmd2BeforeInitializeZero); +APS5_EXPORT("AvPNn0qx4WQ", Hmd2BeforeInitializeZero); +APS5_EXPORT("jp0apqNGgi8", Hmd2BeforeInitializeZero); +APS5_EXPORT("-kZT7X7OMg0", Hmd2BeforeInitializeZero); +APS5_EXPORT("1ewLtng-SVk", Hmd2BeforeInitializeZero); +APS5_EXPORT("B0b8DeIUGes", Hmd2BeforeInitializeZero); +APS5_EXPORT("HclLg4Oj5yw", Hmd2BeforeInitializeZero); +APS5_EXPORT("VbEvrQXsZyY", Hmd2BeforeInitializeZero); +APS5_EXPORT("bB6MKZi5UCQ", Hmd2BeforeInitializeZero); +APS5_EXPORT("OXQ1EDf02XU", Hmd2BeforeInitializeZero); +APS5_EXPORT("IAxfMTvFm0M", Hmd2BeforeInitializeZero); +APS5_EXPORT("qTJtAtaLeno", Hmd2BeforeInitializeZero); +APS5_EXPORT("j7Q16UX6l34", Hmd2BeforeInitializeZero); +APS5_EXPORT("pmmNM06rLqU", Hmd2BeforeInitializeZero); +APS5_EXPORT("zdQfyDK9Ods", Hmd2BeforeInitializeZero); +APS5_EXPORT("aPRwp9GnljM", Hmd2BeforeInitializeZero); +APS5_EXPORT("MjY1gBSaed0", Hmd2BeforeInitializeZero); +APS5_EXPORT("evAMjJYHqW4", Hmd2BeforeInitializeZero); +APS5_EXPORT("r93FLEZ3BBk", Hmd2BeforeInitializeZero); +APS5_EXPORT("IdYURdEdNFo", Hmd2BeforeInitializeZero); +APS5_EXPORT("CVcL2Cu2AEI", Hmd2BeforeInitializeZero); +APS5_EXPORT("NmKIfVxr2s0", Hmd2BeforeInitializeZero); +APS5_EXPORT("KeDLkkp62Gg", Hmd2BeforeInitializeZero); +APS5_EXPORT("+aadu6-ra+A", Hmd2BeforeInitializeZero); +APS5_EXPORT("FgnTiwwGSzk", Hmd2BeforeInitializeZero); +APS5_EXPORT("PWAUwOTHbbo", Hmd2BeforeInitializeZero); +APS5_EXPORT("WzaEApHnsP8", Hmd2BeforeInitializeZero); +APS5_EXPORT("dXzQI8QVY7Y", Hmd2BeforeInitializeZero); +APS5_EXPORT("cCvFKGqRh1k", Hmd2BeforeInitializeZero); +APS5_EXPORT("uj4-2VEAFPU", Hmd2BeforeInitializeZero); +APS5_EXPORT("sIaGStI-iLM", Hmd2BeforeInitializeZero); +APS5_EXPORT("JzwpZOemDO8", Hmd2BeforeInitializeZero); +APS5_EXPORT("IsC4R4bZFVU", Hmd2BeforeInitializeZero); + +std::int32_t APS5_VABI Hmd2BeforeInitializeNotInitialized() { + return BeforeInitialize(Hmd2::NotInitialized, __func__); +} +APS5_EXPORT("sXaPEPY-fLA", Hmd2BeforeInitializeNotInitialized); +APS5_EXPORT("3F5inHhKzXg", Hmd2BeforeInitializeNotInitialized); +APS5_EXPORT("sJAiB-6p55U", Hmd2BeforeInitializeNotInitialized); +APS5_EXPORT("E8hDydLjsSs", Hmd2BeforeInitializeNotInitialized); +APS5_EXPORT("xEQkSb2WJ7Y", Hmd2BeforeInitializeNotInitialized); +APS5_EXPORT("aaP4dnR4q-0", Hmd2BeforeInitializeNotInitialized); +APS5_EXPORT("lEU0TeUoGwI", Hmd2BeforeInitializeNotInitialized); +APS5_EXPORT("cFVyjHBpUUU", Hmd2BeforeInitializeNotInitialized); +APS5_EXPORT("zROtb46jTj4", Hmd2BeforeInitializeNotInitialized); + +std::int32_t APS5_VABI Hmd2BeforeInitializeError0004() { + return BeforeInitialize(Hmd2::Error0004, __func__); +} +APS5_EXPORT("kAMsjYHBbNQ", Hmd2BeforeInitializeError0004); +APS5_EXPORT("5m73zmLpsQ0", Hmd2BeforeInitializeError0004); +APS5_EXPORT("EPeH4xuQ2XQ", Hmd2BeforeInitializeError0004); +APS5_EXPORT("TJ8lyRsjFY4", Hmd2BeforeInitializeError0004); +APS5_EXPORT("-XQVcLnyX68", Hmd2BeforeInitializeError0004); +APS5_EXPORT("giQwQoHhYYA", Hmd2BeforeInitializeError0004); +APS5_EXPORT("zNc9IkgawMI", Hmd2BeforeInitializeError0004); +APS5_EXPORT("-tnG5UJPvAc", Hmd2BeforeInitializeError0004); +APS5_EXPORT("FPhFmG+jXuQ", Hmd2BeforeInitializeError0004); +APS5_EXPORT("CKlkvNngofQ", Hmd2BeforeInitializeError0004); +APS5_EXPORT("yL1CZaciFWE", Hmd2BeforeInitializeError0004); +APS5_EXPORT("qlzgyG3s0RY", Hmd2BeforeInitializeError0004); +APS5_EXPORT("kTIDrDLzMv0", Hmd2BeforeInitializeError0004); +APS5_EXPORT("9Gee0ef8a+U", Hmd2BeforeInitializeError0004); +APS5_EXPORT("ecZVjQdwmBI", Hmd2BeforeInitializeError0004); + +std::int32_t APS5_VABI Hmd2BeforeInitializeParameterNull() { + return BeforeInitialize(Hmd2::ParameterNull, __func__); +} +APS5_EXPORT("MNtiJTDOPAU", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("RRy9WmqZfgU", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("uobR4m9lgwc", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("-XPk5KWBYdo", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("J44idh9izZM", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("Ps0wAvYaa1E", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("kLnMR8oQ1MY", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("kMz4rbBxvjo", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("DIU1sFRRaiE", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("ZQvU4vDzZGQ", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("TJpyGGP9YEY", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("Dih+Odk+RC0", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("oaqdSJw+HVY", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("m35gtHuanXU", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("CTM7QPLJF9Q", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("+rExHeC9lP0", Hmd2BeforeInitializeParameterNull); + +std::int32_t APS5_VABI Hmd2BeforeInitializeParameterInvalid() { + return BeforeInitialize(Hmd2::ParameterInvalid, __func__); +} +APS5_EXPORT("bBh5bCp9coo", Hmd2BeforeInitializeParameterInvalid); +APS5_EXPORT("G3PNDE2VjfY", Hmd2BeforeInitializeParameterInvalid); +APS5_EXPORT("nScCT0OPU0o", Hmd2BeforeInitializeParameterInvalid); +APS5_EXPORT("6Q0Ng7OTN9o", Hmd2BeforeInitializeParameterInvalid); +APS5_EXPORT("-fJKh4SnFR0", Hmd2BeforeInitializeParameterInvalid); +APS5_EXPORT("jUA2s-l0npY", Hmd2BeforeInitializeParameterInvalid); +APS5_EXPORT("+9XhyAIJVj4", Hmd2BeforeInitializeParameterInvalid); +APS5_EXPORT("eBGK-o6E4RQ", Hmd2BeforeInitializeParameterInvalid); +APS5_EXPORT("MTuFeL9184E", Hmd2BeforeInitializeParameterInvalid); + +std::int32_t APS5_VABI Hmd2BeforeInitializeReprojectionNotInitialized() { + return BeforeInitialize(Hmd2::ReprojectionNotInitialized, __func__); +} +APS5_EXPORT("d3SEIYTP6wc", Hmd2BeforeInitializeReprojectionNotInitialized); +APS5_EXPORT("EVECbTvBezo", Hmd2BeforeInitializeReprojectionNotInitialized); +APS5_EXPORT("ciusPdJKjSU", Hmd2BeforeInitializeReprojectionNotInitialized); +APS5_EXPORT("DIwqTkcZiJo", Hmd2BeforeInitializeReprojectionNotInitialized); +APS5_EXPORT("64ERZWVFBTE", Hmd2BeforeInitializeReprojectionNotInitialized); +APS5_EXPORT("z0+0gXUJWs8", Hmd2BeforeInitializeReprojectionNotInitialized); +APS5_EXPORT("rjAd4nCY4DM", Hmd2BeforeInitializeReprojectionNotInitialized); +APS5_EXPORT("Acyg4vTdGm0", Hmd2BeforeInitializeReprojectionNotInitialized); +APS5_EXPORT("aGOWaEeg04I", Hmd2BeforeInitializeReprojectionNotInitialized); +APS5_EXPORT("9up+EPhw1No", Hmd2BeforeInitializeReprojectionNotInitialized); + +std::int32_t APS5_VABI Hmd2BeforeInitializeError002C() { + return BeforeInitialize(Hmd2::Error002C, __func__); +} +APS5_EXPORT("V1kLQmP-17U", Hmd2BeforeInitializeError002C); + +std::int32_t APS5_VABI Hmd2BeforeInitializeError1002() { + return BeforeInitialize(Hmd2::Error1002, __func__); +} +APS5_EXPORT("2LVD1EC5SYE", Hmd2BeforeInitializeError1002); +APS5_EXPORT("Bf87AL6zWxU", Hmd2BeforeInitializeError1002); +APS5_EXPORT("XKt7w1MODFY", Hmd2BeforeInitializeError1002); +APS5_EXPORT("Y5s22M-qUDA", Hmd2BeforeInitializeError1002); +APS5_EXPORT("JJKY-BtQJLg", Hmd2BeforeInitializeError1002); + +std::int32_t APS5_VABI Hmd2BeforeInitializeError1003() { + return BeforeInitialize(Hmd2::Error1003, __func__); +} +APS5_EXPORT("9kj9vw8UShQ", Hmd2BeforeInitializeError1003); + +std::int32_t APS5_VABI Hmd2BeforeInitializeMinusOne() { + return BeforeInitialize(-1, __func__); +} +APS5_EXPORT("EAkv8Mqt33E", Hmd2BeforeInitializeMinusOne); +APS5_EXPORT("Rv7bkmKvmz0", Hmd2BeforeInitializeMinusOne); +APS5_EXPORT("1oPZizn0SNA", Hmd2BeforeInitializeMinusOne); +APS5_EXPORT("TwCpE-nNrlo", Hmd2BeforeInitializeMinusOne); +APS5_EXPORT("jkqAeQfdtxw", Hmd2BeforeInitializeMinusOne); +APS5_EXPORT("+l0RQph81Z8", Hmd2BeforeInitializeMinusOne); +APS5_EXPORT("qsmAtGw1Ncc", Hmd2BeforeInitializeMinusOne); +APS5_EXPORT("uKhwYoMsIsA", Hmd2BeforeInitializeMinusOne); +APS5_EXPORT("WNdaigr94dI", Hmd2BeforeInitializeMinusOne); +APS5_EXPORT("P6S5ZVWf-H0", Hmd2BeforeInitializeMinusOne); +APS5_EXPORT("sun6NWE1sdg", Hmd2BeforeInitializeMinusOne); +APS5_EXPORT("--TFsJYXo04", Hmd2BeforeInitializeMinusOne); + +std::int32_t APS5_VABI Hmd2BeforeInitializeValue0400() { + return BeforeInitialize(0x400, __func__); +} +APS5_EXPORT("BWqcwjCozzI", Hmd2BeforeInitializeValue0400); + +std::int32_t APS5_VABI Hmd2BeforeInitializeValue20000() { + return BeforeInitialize(0x20000, __func__); +} +APS5_EXPORT("PRz7TdBGUI0", Hmd2BeforeInitializeValue20000); + +int APS5_VABI sceHmd2InternalSetPortStatus(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceHmd2SetIpdOffset() { +int APS5_VABI sceHmd2InternalLedDetectorStartBySlot(void) { NotImplemented_nid_no_patch(__func__); return 0; } +int APS5_VABI sceHmd2InternalImageStartBySlot(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalSetPortConnection(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalSetCameraSeeThroughMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalSetFakeLensSeparationDistance(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalDfuSend(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalLedDetectorOpenBySlot(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalImageOpenBySlot(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalLedDetectorStopBySlot(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalSetDeviceConnection(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalLedDetectorCloseBySlot(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalSetSidetone(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalGetDeviceId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalSetBrightness(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalImageStopBySlot(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalSetVibrationMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalGetVr2dData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2InternalImageCloseBySlot(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +std::int32_t APS5_VABI Hmd2CheckFirstThenError0004(const void* a0) { + if (a0 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::Error0004, __func__); +} +APS5_EXPORT("9WeZr4kYfzk", Hmd2CheckFirstThenError0004); +APS5_EXPORT("N+5h1hCOFAg", Hmd2CheckFirstThenError0004); +APS5_EXPORT("w2vEs2y5ZmE", Hmd2CheckFirstThenError0004); +APS5_EXPORT("L5gat1pe0y0", Hmd2CheckFirstThenError0004); +APS5_EXPORT("4GnLHR9NJ1w", Hmd2CheckFirstThenError0004); +APS5_EXPORT("AHk7nGSzef0", Hmd2CheckFirstThenError0004); +APS5_EXPORT("ncjX9d3mc0M", Hmd2CheckFirstThenError0004); +APS5_EXPORT("l63QYAUuogc", Hmd2CheckFirstThenError0004); +APS5_EXPORT("mrcBySCdXSo", Hmd2CheckFirstThenError0004); +APS5_EXPORT("ieBra+td8sU", Hmd2CheckFirstThenError0004); +APS5_EXPORT("s+rzBnewgGs", Hmd2CheckFirstThenError0004); +APS5_EXPORT("Nfcm0GBdzZI", Hmd2CheckFirstThenError0004); +APS5_EXPORT("Tc9rasWfrm4", Hmd2CheckFirstThenError0004); +APS5_EXPORT("ckTbKTX4KZg", Hmd2CheckFirstThenError0004); +APS5_EXPORT("D7576PUbUjk", Hmd2CheckFirstThenError0004); +APS5_EXPORT("tTJ04SrBT8U", Hmd2CheckFirstThenError0004); +APS5_EXPORT("sDP7hZ-P6OI", Hmd2CheckFirstThenError0004); +APS5_EXPORT("J+eVNbN9ndY", Hmd2CheckFirstThenError0004); +APS5_EXPORT("kozvHBNyxf0", Hmd2CheckFirstThenError0004); +APS5_EXPORT("638Tmg5y6U4", Hmd2CheckFirstThenError0004); + +std::int32_t APS5_VABI Hmd2CheckFirstThenZero(const void* a0) { + if (a0 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(0, __func__); +} +APS5_EXPORT("8ETtFtp5zqY", Hmd2CheckFirstThenZero); +APS5_EXPORT("CWPkBm3EXQA", Hmd2CheckFirstThenZero); +APS5_EXPORT("R62liQQIAaA", Hmd2CheckFirstThenZero); +APS5_EXPORT("-bcWLHsdzo0", Hmd2CheckFirstThenZero); +APS5_EXPORT("JngJzgNbu8U", Hmd2CheckFirstThenZero); +APS5_EXPORT("xNBSz8xrGdk", Hmd2CheckFirstThenZero); +APS5_EXPORT("yUVPfN-jRIU", Hmd2CheckFirstThenZero); +APS5_EXPORT("BKPR3RZ5aO4", Hmd2CheckFirstThenZero); + +std::int32_t APS5_VABI Hmd2CheckFirstThenError1002(const void* a0) { + if (a0 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::Error1002, __func__); +} +APS5_EXPORT("IG4y+drWAWw", Hmd2CheckFirstThenError1002); +APS5_EXPORT("rhYUml9vppE", Hmd2CheckFirstThenError1002); +APS5_EXPORT("6574xHS+jZk", Hmd2CheckFirstThenError1002); +APS5_EXPORT("mXzaY6xVoxg", Hmd2CheckFirstThenError1002); +APS5_EXPORT("6af-HIZQQn4", Hmd2CheckFirstThenError1002); +APS5_EXPORT("6qWtdTHQsTk", Hmd2CheckFirstThenError1002); +APS5_EXPORT("pUm8t8xWhFg", Hmd2CheckFirstThenError1002); +APS5_EXPORT("A0XKI-WjxK8", Hmd2CheckFirstThenError1002); + +std::int32_t APS5_VABI Hmd2CheckFirstThenMinusOne(const void* a0) { + if (a0 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(-1, __func__); +} +APS5_EXPORT("A9s2j8OioPQ", Hmd2CheckFirstThenMinusOne); +APS5_EXPORT("YIgtmrL+HLY", Hmd2CheckFirstThenMinusOne); +APS5_EXPORT("Ng8zEkGxDNs", Hmd2CheckFirstThenMinusOne); +APS5_EXPORT("EtslTnqO1xU", Hmd2CheckFirstThenMinusOne); +APS5_EXPORT("hfrnRTryKQQ", Hmd2CheckFirstThenMinusOne); + +std::int32_t APS5_VABI Hmd2CheckFirstTwoThenParameterInvalid(const void* a0, const void* a1) { + if (a0 == nullptr) return Hmd2::ParameterNull; + if (a1 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::ParameterInvalid, __func__); +} +APS5_EXPORT("PPX68fNToFM", Hmd2CheckFirstTwoThenParameterInvalid); +APS5_EXPORT("LxPIfjHaY4c", Hmd2CheckFirstTwoThenParameterInvalid); +APS5_EXPORT("w8gBTTdg+po", Hmd2CheckFirstTwoThenParameterInvalid); + +std::int32_t APS5_VABI Hmd2CheckSecondThenError002C(const void* a0, const void* a1) { + (void)a0; + if (a1 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::Error002C, __func__); +} +APS5_EXPORT("tWJPX2ycRMM", Hmd2CheckSecondThenError002C); +APS5_EXPORT("5UMB9PJA70o", Hmd2CheckSecondThenError002C); +APS5_EXPORT("xQs62qCk1EI", Hmd2CheckSecondThenError002C); + +std::int32_t APS5_VABI Hmd2CheckFirstThenParameterInvalid(const void* a0) { + if (a0 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::ParameterInvalid, __func__); +} +APS5_EXPORT("MMc+bSlqkzk", Hmd2CheckFirstThenParameterInvalid); +APS5_EXPORT("NpZHgvSYgEA", Hmd2CheckFirstThenParameterInvalid); + +std::int32_t APS5_VABI Hmd2CheckAllSixThenZero(const void* a0, const void* a1, const void* a2, const void* a3, const void* a4, const void* a5) { + if (a0 == nullptr) return Hmd2::ParameterNull; + if (a1 == nullptr) return Hmd2::ParameterNull; + if (a2 == nullptr) return Hmd2::ParameterNull; + if (a3 == nullptr) return Hmd2::ParameterNull; + if (a4 == nullptr) return Hmd2::ParameterNull; + if (a5 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(0, __func__); +} +APS5_EXPORT("z-serf9vsb0", Hmd2CheckAllSixThenZero); +APS5_EXPORT("IRMUFu+uG5s", Hmd2CheckAllSixThenZero); + +std::int32_t APS5_VABI Hmd2CheckSecondThenParameterInvalid(const void* a0, const void* a1) { + (void)a0; + if (a1 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::ParameterInvalid, __func__); +} +APS5_EXPORT("72U64E+0z6U", Hmd2CheckSecondThenParameterInvalid); +APS5_EXPORT("LCQzi5ILGrY", Hmd2CheckSecondThenParameterInvalid); + +std::int32_t APS5_VABI Hmd2CheckFirstThenError800d0203(const void* a0) { + if (a0 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::Error800d0203, __func__); +} +APS5_EXPORT("Im0qL0j9Ubg", Hmd2CheckFirstThenError800d0203); + +std::int32_t APS5_VABI Hmd2CheckFirstThenError1003(const void* a0) { + if (a0 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::Error1003, __func__); +} +APS5_EXPORT("ng6eI8uA0h4", Hmd2CheckFirstThenError1003); + +std::int32_t APS5_VABI Hmd2CheckFirstThenError1010(const void* a0) { + if (a0 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::Error1010, __func__); +} +APS5_EXPORT("mPPGgjyJrCI", Hmd2CheckFirstThenError1010); + +std::int32_t APS5_VABI Hmd2CheckFirstTwoThenError1002(const void* a0, const void* a1) { + if (a0 == nullptr) return Hmd2::ParameterNull; + if (a1 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::Error1002, __func__); +} +APS5_EXPORT("ZupCJmV5BDI", Hmd2CheckFirstTwoThenError1002); + +std::int32_t APS5_VABI Hmd2CheckFirstTwoThenError1003(const void* a0, const void* a1) { + if (a0 == nullptr) return Hmd2::ParameterNull; + if (a1 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::Error1003, __func__); +} +APS5_EXPORT("mAQZwOX1IHU", Hmd2CheckFirstTwoThenError1003); + +std::int32_t APS5_VABI Hmd2CheckFirstAndThirdThenZero(const void* a0, const void* a1, const void* a2) { + if (a0 == nullptr) return Hmd2::ParameterNull; + (void)a1; + if (a2 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(0, __func__); +} +APS5_EXPORT("oacDkQkinLo", Hmd2CheckFirstAndThirdThenZero); + +std::int32_t APS5_VABI Hmd2CheckFirstAndThirdThenError1003(const void* a0, const void* a1, const void* a2) { + if (a0 == nullptr) return Hmd2::ParameterNull; + (void)a1; + if (a2 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::Error1003, __func__); +} +APS5_EXPORT("zn7PHGzzEyI", Hmd2CheckFirstAndThirdThenError1003); + +std::int32_t APS5_VABI Hmd2CheckSecondThenInvalidHandle(const void* a0, const void* a1) { + (void)a0; + if (a1 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::InvalidHandle, __func__); +} +APS5_EXPORT("wEHILc7TP04", Hmd2CheckSecondThenInvalidHandle); + +std::int32_t APS5_VABI Hmd2CheckSecondThenError1002(const void* a0, const void* a1) { + (void)a0; + if (a1 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::Error1002, __func__); +} +APS5_EXPORT("W-ZIlUXcrNA", Hmd2CheckSecondThenError1002); + + +std::int32_t APS5_VABI Hmd2WriteStateTwo(void* a0) { + if (a0 == nullptr) return Hmd2::ParameterNull; + static constexpr std::uint8_t bytes[] = {0x02, 0x00, 0x00, 0x00, 0x00}; + std::memcpy(a0, bytes, sizeof bytes); + return BeforeInitialize(0, __func__); +} +APS5_EXPORT("omimgG5itSs", Hmd2WriteStateTwo); + +std::int32_t APS5_VABI Hmd2WriteSize0x2000Align0x100(std::uint64_t* a0, std::uint64_t* a1) { + if (a0 == nullptr || a1 == nullptr) return Hmd2::ParameterNull; + *a0 = 0x2000; + *a1 = 0x100; + return BeforeInitialize(0, __func__); +} +APS5_EXPORT("6PeHcAaKYg8", Hmd2WriteSize0x2000Align0x100); + +std::int32_t APS5_VABI Hmd2WriteFourConstants(void* a0) { + if (a0 == nullptr) return Hmd2::ParameterNull; + static constexpr std::uint8_t bytes[] = {0xe1, 0xf0, 0x74, 0x3c, 0xd8, 0xf0, 0x74, 0x3c, 0x1f, 0xf1, 0x74, 0x3c, 0xd8, 0xf0, 0x74, 0x3c}; + std::memcpy(a0, bytes, sizeof bytes); + return BeforeInitialize(0, __func__); +} +APS5_EXPORT("yur+xPdWPAg", Hmd2WriteFourConstants); + +std::int32_t APS5_VABI Hmd2WriteSixHalvesThenInvalidHandle(std::uint64_t a0, void* a1) { + (void)a0; + if (a1 == nullptr) return Hmd2::ParameterNull; + static constexpr std::uint8_t bytes[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x3f}; + std::memcpy(a1, bytes, sizeof bytes); + return BeforeInitialize(Hmd2::InvalidHandle, __func__); +} +APS5_EXPORT("RnieDDp5ge4", Hmd2WriteSixHalvesThenInvalidHandle); +APS5_EXPORT("jM3NEJA11yw", Hmd2WriteSixHalvesThenInvalidHandle); + +std::int32_t APS5_VABI Hmd2WriteTwelveHalvesThenInvalidHandle(std::uint64_t a0, void* a1) { + (void)a0; + if (a1 == nullptr) return Hmd2::ParameterNull; + static constexpr std::uint8_t bytes[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + std::memcpy(a1, bytes, sizeof bytes); + return BeforeInitialize(Hmd2::InvalidHandle, __func__); +} +APS5_EXPORT("b0H-n-tUk0U", Hmd2WriteTwelveHalvesThenInvalidHandle); + +std::int32_t APS5_VABI Hmd2SlotUpTo3CheckSecondThirdThenZero(std::uint64_t a0, const void* a1, const void* a2) { + if (a1 == nullptr || a2 == nullptr) return Hmd2::ParameterNull; + if (a0 > 3) return Hmd2::ParameterInvalid; + return BeforeInitialize(0, __func__); +} +APS5_EXPORT("MXhloJ6Iw3w", Hmd2SlotUpTo3CheckSecondThirdThenZero); + +std::int32_t APS5_VABI Hmd2SlotUpTo1ThenError1002(std::uint64_t a0) { + if (a0 > 1) return Hmd2::ParameterInvalid; + return BeforeInitialize(Hmd2::Error1002, __func__); +} +APS5_EXPORT("19wrIIiqFgM", Hmd2SlotUpTo1ThenError1002); + +std::int32_t APS5_VABI Hmd2SlotUpTo1CheckSecondThenError1002(std::uint64_t a0, const void* a1) { + if (a1 == nullptr) return Hmd2::ParameterNull; + if (a0 > 1) return Hmd2::ParameterInvalid; + return BeforeInitialize(Hmd2::Error1002, __func__); +} +APS5_EXPORT("Z389vAm47Ys", Hmd2SlotUpTo1CheckSecondThenError1002); + +std::int32_t APS5_VABI Hmd2SlotUpTo1ThenMinusOne(std::uint64_t a0) { + if (a0 > 1) return Hmd2::ParameterInvalid; + return BeforeInitialize(-1, __func__); +} +APS5_EXPORT("cDcrvEG8j8k", Hmd2SlotUpTo1ThenMinusOne); +APS5_EXPORT("tuqJO6O6cb4", Hmd2SlotUpTo1ThenMinusOne); +APS5_EXPORT("fd4fu1cziaI", Hmd2SlotUpTo1ThenMinusOne); + +std::int32_t APS5_VABI Hmd2SlotUpTo2ThenMinusOne(std::uint64_t a0) { + if (a0 > 2) return Hmd2::ParameterInvalid; + return BeforeInitialize(-1, __func__); +} +APS5_EXPORT("UxMg7iL4nRs", Hmd2SlotUpTo2ThenMinusOne); +APS5_EXPORT("K1GELwlaaGc", Hmd2SlotUpTo2ThenMinusOne); + +std::int32_t APS5_VABI Hmd2SlotUpTo2ThenError1002(std::uint64_t a0) { + if (a0 > 2) return Hmd2::ParameterInvalid; + return BeforeInitialize(Hmd2::Error1002, __func__); +} +APS5_EXPORT("RFn39VCHjGY", Hmd2SlotUpTo2ThenError1002); + +std::int32_t APS5_VABI Hmd2SlotUpTo2ThenError1003(std::uint64_t a0) { + if (a0 > 2) return Hmd2::ParameterInvalid; + return BeforeInitialize(Hmd2::Error1003, __func__); +} +APS5_EXPORT("FRoSjceDv10", Hmd2SlotUpTo2ThenError1003); + +std::int32_t APS5_VABI Hmd2SlotUpTo2CheckSecondThenError002C(std::uint64_t a0, const void* a1) { + if (a1 == nullptr) return Hmd2::ParameterNull; + if (a0 > 2) return Hmd2::ParameterInvalid; + return BeforeInitialize(Hmd2::Error002C, __func__); +} +APS5_EXPORT("wQ44gBwBw44", Hmd2SlotUpTo2CheckSecondThenError002C); + +std::int32_t APS5_VABI Hmd2IndexZeroCheckSecondThenZero(std::uint64_t a0, const void* a1) { + if (a1 == nullptr) return Hmd2::ParameterNull; + if (a0 != 0) return Hmd2::Error100F; + return BeforeInitialize(0, __func__); +} +APS5_EXPORT("wiv8+7ogrq0", Hmd2IndexZeroCheckSecondThenZero); + } diff --git a/core/libs/prx/libSceHmd2/Hmd2.hpp b/core/libs/prx/libSceHmd2/Hmd2.hpp index 3594b32bd1..c5ae98b060 100644 --- a/core/libs/prx/libSceHmd2/Hmd2.hpp +++ b/core/libs/prx/libSceHmd2/Hmd2.hpp @@ -14,6 +14,14 @@ inline constexpr std::int32_t InvalidHandle = static_cast(0x8a7200 inline constexpr std::int32_t AlreadyOpened = static_cast(0x8a720006); inline constexpr std::int32_t ParameterNull = static_cast(0x8a720008); inline constexpr std::int32_t ParameterInvalid = static_cast(0x8a720009); +inline constexpr std::int32_t Error0004 = static_cast(0x8a720004); +inline constexpr std::int32_t ReprojectionNotInitialized = static_cast(0x8a72000b); +inline constexpr std::int32_t Error002C = static_cast(0x8a72002c); +inline constexpr std::int32_t Error1002 = static_cast(0x8a721002); +inline constexpr std::int32_t Error1003 = static_cast(0x8a721003); +inline constexpr std::int32_t Error1010 = static_cast(0x8a721010); +inline constexpr std::int32_t Error800d0203 = static_cast(0x800d0203); +inline constexpr std::int32_t Error100F = static_cast(0x8a72100f); inline constexpr std::uint32_t UserIdSdkVersion = 0x07000000; @@ -49,6 +57,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 +76,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..19bec94c78 100644 --- a/core/libs/prx/libSceHttp/Export.cpp +++ b/core/libs/prx/libSceHttp/Export.cpp @@ -481,35 +481,42 @@ int APS5_VABI sceHttpParseResponseHeader(const char* header, std::size_t headerL } if (!found) return notFound; - while (start < headerLen && isSpace(input[start])) { - if (input[start++] == '\n') break; - } - std::size_t end = headerLen; - std::size_t consumed = headerLen; - std::size_t scan = start; - while (scan < headerLen) { - const auto newline = input.find('\n', scan); - if (newline == std::string_view::npos) break; - const auto next = newline + 1; - if (next < headerLen && (input[next] == ' ' || input[next] == '\t')) { - scan = next; - continue; - } - end = newline > start && input[newline - 1] == '\r' ? newline - 1 : newline; - consumed = next; - break; - } - if (end == start) { + const auto continues = [&input, headerLen](std::size_t next) { return next < headerLen && (input[next] == ' ' || input[next] == '\t'); }; + const auto empty = [fieldValue, valueLen] { *fieldValue = nullptr; *valueLen = 0; return 0; + }; + while (start < headerLen) { + const std::size_t lineFeed = input[start] == '\r' && start + 1 < headerLen && input[start + 1] == '\n' ? start + 1 : start; + if (input[lineFeed] == '\n') { + if (!continues(lineFeed + 1)) return empty(); + start = lineFeed + 1; + } else if (isSpace(input[start])) { + ++start; + } else { + break; + } } - if (consumed > static_cast(std::numeric_limits::max())) { - throw std::out_of_range("sceHttpParseResponseHeader: consumed byte count exceeds int"); + if (start == headerLen) return empty(); + std::size_t end = headerLen; + std::size_t scan = start; + while (scan < headerLen) { + const auto newline = input.find('\n', scan); + if (newline == std::string_view::npos || newline + 1 == headerLen) break; + if (continues(newline + 1)) { + scan = newline + 1; + continue; + } + end = newline > start && input[newline - 1] == '\r' ? newline - 1 : newline; + break; + } + if (end > static_cast(std::numeric_limits::max())) { + throw std::out_of_range("sceHttpParseResponseHeader: value end exceeds int"); } *fieldValue = header + start; *valueLen = end - start; - return static_cast(consumed); + return static_cast(end); } int APS5_VABI sceHttpCreateRequest2(int conn_id, const char* method, const char* path, uint64_t content_length) { @@ -609,4 +616,248 @@ 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) { + return 0; +} + +int APS5_VABI sceHttpDbgSetPrintf(void) { + 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..670a29e7eb 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; @@ -218,21 +229,31 @@ int APS5_VABI sceHttpUriEscape(char* out, size_t* require, size_t prepare, const } int APS5_VABI sceHttpUriUnescape(char* out, size_t* require, size_t prepare, const char* in) { - if (!in) return ERROR_INVALID_VALUE; + if (!in || (!out && !require)) return ERROR_INVALID_VALUE; + const auto hexDigit = [](char c) -> int { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; + }; const std::string_view input(in); std::string decoded; for (size_t i = 0; i < input.size(); ++i) { - if (input[i] == '%' && input.size() - i >= 3) { - unsigned int value = 0; - const auto result = std::from_chars(input.data() + i + 1, input.data() + i + 3, value, 16); - if (result.ec == std::errc{} && result.ptr == input.data() + i + 3) { - decoded.push_back(static_cast(value)); - i += 2; - continue; - } + if (input[i] != '%') { + decoded.push_back(input[i]); + continue; } - decoded.push_back(input[i]); + if (input.size() - i < 3) return ERROR_INVALID_URL; + const int high = hexDigit(input[i + 1]); + const int low = hexDigit(input[i + 2]); + if (high < 0 || low < 0) return ERROR_INVALID_URL; + decoded.push_back(static_cast(high * 16 + low)); + i += 2; + } + if (out && prepare < decoded.size() + 1) { + std::memcpy(out, decoded.data(), std::min(prepare, decoded.size())); + return ERROR_OUT_OF_MEMORY; } return copyOut(decoded, out, require, prepare); } @@ -355,7 +376,7 @@ int APS5_VABI sceHttpUriSweepPath(char* dst, const char* src, size_t srcSize) { if (!dst || !src) return ERROR_INVALID_VALUE; const size_t length = srcSize - 1; - if (length == 0 || src[0] != '/') { + if (src[0] != '/') { std::memcpy(dst, src, length); dst[length] = '\0'; return 0; @@ -370,7 +391,7 @@ int APS5_VABI sceHttpUriSweepPath(char* dst, const char* src, size_t srcSize) { pos += 2; continue; } - if (src[pos] == '.' && pos + 2 < length && src[pos + 1] == '.' && src[pos + 2] == '/') { + if (src[pos] == '.' && pos + 2 <= length && src[pos + 1] == '.' && src[pos + 2] == '/') { if (end != 0) { dst[end] = '\0'; end = static_cast(std::strrchr(dst, '/') - dst); diff --git a/core/libs/prx/libSceHttp2/Export.cpp b/core/libs/prx/libSceHttp2/Export.cpp index af3cde75ba..3d681f4ede 100644 --- a/core/libs/prx/libSceHttp2/Export.cpp +++ b/core/libs/prx/libSceHttp2/Export.cpp @@ -410,5 +410,94 @@ int APS5_VABI sceHttp2WebSocketSetPingTimeout() { NotImplemented_nid_no_patch(__func__); return 0; } +int APS5_VABI sceHttp2AddCookie(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttp2AuthCacheFlush(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttp2CookieExport(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttp2CookieImport(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttp2GetAuthEnabled(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttp2GetAutoRedirect(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttp2GetCookie(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttp2GetCookieBox(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttp2GetCookieStats(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttp2RedirectCacheFlush(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttp2RemoveRequestHeader(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttp2SetAuthInfoCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttp2SetCookieMaxNum(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttp2SetCookieMaxNumPerDomain(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttp2SetCookieMaxSize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttp2SetCookieRecvCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttp2SetCookieSendCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttp2SetPreSendCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceHttpCache/Export.cpp b/core/libs/prx/libSceHttpCache/Export.cpp index fb34206c21..6f5d37ccb1 100644 --- a/core/libs/prx/libSceHttpCache/Export.cpp +++ b/core/libs/prx/libSceHttpCache/Export.cpp @@ -5,6 +5,138 @@ 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() { + return 0; +} + +int APS5_VABI sceHttpCacheSystemShutdown() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheSystemTerm() { + return 0; +} + +int APS5_VABI sceHttpCacheTerm() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + + +int APS5_VABI sceHttpCacheClearAllUserTags() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheClearCacheInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheDebugSetUrl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheRegisterCacheInfoUrl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpCacheRemoveTag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceIme/CMakeLists.txt b/core/libs/prx/libSceIme/CMakeLists.txt index 5f45b0907c..460b1fed31 100644 --- a/core/libs/prx/libSceIme/CMakeLists.txt +++ b/core/libs/prx/libSceIme/CMakeLists.txt @@ -23,6 +23,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(guest_ime_filter_text_tests ${LIBS_INCLUDE_DIR}/tests/GuestImeFilterText.cpp) + target_include_directories(guest_ime_filter_text_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_ime_filter_text_tests PRIVATE libSceIme libc) + configure_windows_unwind(guest_ime_filter_text_tests) + add_test(NAME guest_ime_filter_text COMMAND guest_ime_filter_text_tests) + if(WIN32) + set_tests_properties(guest_ime_filter_text PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_ime_open_tests ${LIBS_INCLUDE_DIR}/tests/GuestImeOpen.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../libSceVideoOut/src/ImeTextInput.cpp) target_include_directories(guest_ime_open_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR}) diff --git a/core/libs/prx/libSceIme/Export.cpp b/core/libs/prx/libSceIme/Export.cpp index 575463cf73..6eaf4cbbe8 100644 --- a/core/libs/prx/libSceIme/Export.cpp +++ b/core/libs/prx/libSceIme/Export.cpp @@ -9,8 +9,10 @@ #include #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libSceIme/include/ImeFilterText.hpp" #include "prx/libSceIme/include/ImeInput.hpp" namespace { @@ -38,6 +40,7 @@ constexpr int ErrorInvalidMode = static_cast(0x80bc0024u); constexpr int ErrorInvalidParam = static_cast(0x80bc0030u); constexpr int ErrorInvalidAddress = static_cast(0x80bc0031u); constexpr int ErrorInvalidReserved = static_cast(0x80bc0032u); +constexpr int ErrorInternal = static_cast(0x80bc00ffu); constexpr uint32_t ValidKeyboardModes = 0x0000007f; constexpr uint32_t TypeNumber = 4; constexpr uint32_t ValidOptions = 0x00007bff; @@ -59,9 +62,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 +80,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 +169,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 +240,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 +260,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 +358,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 +386,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); @@ -400,6 +469,16 @@ int APS5_VABI sceImeKeyboardSetMode(int32_t user_id, uint32_t mode) { return (mode & ~ValidKeyboardModes) == 0 ? 0 : ErrorInvalidMode; } +int APS5_VABI sceImeFilterText(char16_t* dst, int32_t* out_length, uint32_t capacity, + const ime_filter::FilterParam* param, const char16_t* src, uint32_t src_length) { + return static_cast(ime_filter::FilterText(dst, out_length, capacity, param, src, src_length)); +} + +int APS5_VABI sceImeCheckFilterText(const void* param) { + (void)param; + NotImplemented_nid_no_patch(__func__); +} + void APS5_VABI sceImeParamInit(Param* param) { if (!param) return; std::memset(param, 0, sizeof(*param)); @@ -412,6 +491,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 +509,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 +528,280 @@ 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) { + return ErrorInternal; +} + +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; +} +int APS5_VABI FinalizeImeModule_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI InitializeImeModule_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceIme/include/ImeFilterCodepages.hpp b/core/libs/prx/libSceIme/include/ImeFilterCodepages.hpp new file mode 100644 index 0000000000..4249c9b1d7 --- /dev/null +++ b/core/libs/prx/libSceIme/include/ImeFilterCodepages.hpp @@ -0,0 +1,299 @@ +#pragma once +#include + +namespace ime_filter { + +inline constexpr std::uint64_t kCodepageABitmap[1024] = { + 0xffffffffffffffffull, 0xffffffffffffffffull, 0xffffffff00000000ull, 0xffffffffffffffffull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0xfffe03fbfffe0000ull, 0x00000000000003fbull, + 0xffffffffffff0002ull, 0x000000000002ffffull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x480d006333710000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000080200400008ull, 0x03ff03ff00000000ull, 0x00000000000f0000ull, 0x0000000000140000ull, + 0x20305fa1e406098dull, 0x00000cc300040000ull, 0x80000020000000ccull, 0x0000000000000000ull, + 0x0000000000040000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x000fffff00000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x999999393999900full, 0x0000000000000804ull, 0x300c000300000000ull, 0x000080000000c8c0ull, + 0x0000000000000060ull, 0x0000a40000000005ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x00000000b03fffefull, 0xfffffffffffffffeull, 0xfffffffe780fffffull, 0x787fffffffffffffull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0206000000000000ull, 0x0000000000000000ull, 0x000001f000000000ull, 0x0000000000000000ull, + 0x084008cc01102008ull, 0x7800000000822600ull, 0x000000027000c000ull, 0x0000000000002010ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x9b46254243f36f8bull, 0x400a0004e3e0e82cull, 0x04497977db365f65ull, 0x18c5603ae3f0ecd7ull, + 0x375180003403e60bull, 0x986982007eebe0c8ull, 0x8060e8032d56ad48ull, 0xc568c03aad93661cull, + 0x02403f7ec656aa60ull, 0x21751020146183cdull, 0x40bc300007122021ull, 0x0a3060a84562a624ull, + 0x9c84040285740217ull, 0x11e27f3414157ffbull, 0x60ff1f7522efb665ull, 0x676336c338403a70ull, + 0x0fc946b020b24dd9ull, 0xa03f86384850bc98ull, 0x5232be4998162388ull, 0xc72c00ddeba422abull, + 0x8f0a841b26e1a1e7ull, 0x89bfc241559e27ebull, 0x084d636185480014ull, 0x05cfff3eaad07f0cull, + 0x7b407a41a803ff1aull, 0x38eb050080024745ull, 0x710c9b341005dc51ull, 0xa404636601000397ull, + 0x430ac000005180d0ull, 0x5800000830c89071ull, 0x00415f80f7000ed9ull, 0x62800018941000b0ull, + 0x0156820009d00240ull, 0x05101d1008015004ull, 0x10504025001084c1ull, 0xa60d40094d8a410full, + 0x098121c0914cab19ull, 0x800006720203c485ull, 0x0009141d00080b04ull, 0x16900009905c49c9ull, + 0x2433841222200c65ull, 0x42250a0447960c03ull, 0x4f0c4900d0880028ull, 0x3e87d830d3aa14a2ull, + 0x41867ea41f618e04ull, 0x211857ad2dbbc390ull, 0x4e0411382a48241eull, 0x88400d60161b0a40ull, + 0x106082219502020aull, 0x8000144404000243ull, 0x700000000c040000ull, 0x0c00024a00c11a06ull, + 0x4045140400401a00ull, 0x052b0a78bdf30029ull, 0x8379407cbfa0bba9ull, 0xc5695bf6e91d12fdull, + 0xff022115444aeff6ull, 0x0242d033402bed63ull, 0x5dca1b4200131000ull, 0x2c61a703020000a0ull, + 0x000002848ff24880ull, 0x0048b200100d5804ull, 0x3780500420011894ull, 0x68be49ea684d3200ull, + 0x21c9a8202e42184cull, 0xff7c001e80b050b9ull, 0x01e028c114e0849aull, 0xdddb130fac49870eull, + 0x51b2a2e289fbbe1aull, 0x928b3ec632ca5522ull, 0x32986703438f1dbfull, 0xa923081173c03028ull, + 0x04028fe33a65c000ull, 0x00a1bf3da6252c4eull, 0x317c06c98cd43e3aull, 0x0edf018bd52a00e0ull, + 0xf09111838c22e34bull, 0x40fbc9aca7287d94ull, 0x44445a9007534484ull, 0xf5d4004800013fc8ull, + 0x891dc442ec5f7701ull, 0xd242410949286b83ull, 0x3a22184059fe061dull, 0xc0eaf0033b9fb7e4ull, + 0xe400898082021386ull, 0x0cc44b8010a1b200ull, 0x48341faf8944d309ull, 0x0470420a0c458259ull, + 0x4450314010c8a040ull, 0x0540828101004004ull, 0x1a056a30642c0108ull, 0x645690cf051460a6ull, + 0xcbf09c1831000021ull, 0x01b5104c63e2e120ull, 0x3281b8b29a83538cull, 0x0c0233e70a84987aull, + 0x9872e1b1d038d6cdull, 0x0459c3f4e2848a1eull, 0xd314484523c2439aull, 0xffbd024136400292ull, + 0xa5d27dc0e8f0eb09ull, 0xd0afa47fd24bc242ull, 0x0bd8824734a11aa0ull, 0xc83ad294651bc453ull, + 0x33140e0640c8001eull, 0xc0d00088b21f615full, 0x166ba1c5a898a02aull, 0x0604c08b85b4af50ull, + 0xa251056e1e04f933ull, 0x73b8ed0776380400ull, 0xc816408119324406ull, 0xaa04298463097c8aull, + 0x27614e0eca9c1c24ull, 0xc10c0846830009d0ull, 0x0908540d10816011ull, 0x0c000514cc0a000eull, + 0x6784008ba0440430ull, 0x8b18865e8a195288ull, 0x9cbe8c1041602e59ull, 0x00089800895c6861ull, + 0xc1900018089a8100ull, 0x640d8505f4a14007ull, 0xff0a48060e4d314eull, 0x000b852e2ea81632ull, + 0x696c0e20ca841810ull, 0x0390d65816000032ull, 0x112490001a6851a0ull, 0x1fae5d52432698e1ull, + 0x5700fafbae280fa0ull, 0xc044c88099406408ull, 0xa4c48424b1419005ull, 0xc1949000603a1a34ull, + 0xc106180d003a8246ull, 0x1511e05099100022ull, 0x022a041a00824157ull, 0x446ad8138930004full, + 0x400511c0ed228aa2ull, 0x3101880801021000ull, 0x0f08f80002044620ull, 0x22020000a2008900ull, + 0x1040004216108210ull, 0x200052f4126052c0ull, 0x4202110082308510ull, 0xda2070e180b5430aull, + 0xfc65350008012040ull, 0x62140286ab0419c1ull, 0x4246908500440087ull, 0x338032070a85405cull, + 0xc0d0ce30b8c00400ull, 0x0da505080080c030ull, 0x280c020000400a90ull, 0x4122642940446705ull, + 0x847c4664000002e8ull, 0x4049861dde200002ull, 0x20010084c0000a08ull, 0x01c742cd10108400ull, + 0x1d8f9968d52a703aull, 0x81d9aef53e12be50ull, 0x732e08282412cec4ull, 0xd41d020c4b3424acull, + 0x0811009780002a02ull, 0x7d451786114411c4ull, 0x879140405e4949ddull, 0x491444bad8c4254cull, + 0x15800271c8001b92ull, 0xc200096a0c0000c1ull, 0xba49302140024800ull, 0x1008e2ac1c802080ull, + 0x841400e300341004ull, 0x1414981020004020ull, 0x5420868804aa70c2ull, 0x2010918004130c62ull, + 0x54011c4002064082ull, 0x84802125e4e90383ull, 0xe60944c02810e433ull, 0x080112da81260a03ull, + 0xf886400197906901ull, 0xa6510a0e0081e24dull, 0x8441c60081ec011aull, 0x8741acefb62eadb8ull, + 0x026811614b028d54ull, 0x043350a02057bb60ull, 0x01122402f7b4a8c0ull, 0x00c8227120009ad3ull, + 0xe1800c8a809e2081ull, 0x402810318151b009ull, 0x620e69b689a52a0eull, 0x4d548085d1444425ull, + 0x862dd8071fb12c75ull, 0x226e414e5841d97cull, 0xedb7f80d9e088200ull, 0x0814931375668c80ull, + 0x6ea6484ec8040e32ull, 0xba0126c066742c4aull, 0x00000000185dd70cull, 0x0000000000000000ull, + 0x0540000000000000ull, 0x03a54f81813370a0ull, 0x2344c31a641055ecull, 0x1a090a4300341462ull, + 0xa848010213a5187bull, 0xe2dd8106c5440440ull, 0x0416b6262d481af0ull, 0x311280326e405058ull, + 0x420a82080c0007e4ull, 0x87134860803b4840ull, 0xe52903193428850dull, 0x5c1825a9870a2345ull, + 0x03e85e00d9c577a6ull, 0x41c6cd54a7000081ull, 0x2b0ab860a2042800ull, 0x0e1a08eada9e0020ull, + 0x0376890811c0427eull, 0x98a8000401058621ull, 0x20220d05c44846a0ull, 0x28d78a01914854a2ull, + 0x3122160500087898ull, 0x06a2fa4e08804340ull, 0x9b14200292110814ull, 0x9010500016432e52ull, + 0x2020304285ba0041ull, 0x40802f0807a84f0bull, 0x0601df501a930591ull, 0x4e8006303021a202ull, + 0x8001a00404c80cc4ull, 0x0a020880d4316000ull, 0x00418e1800281c00ull, 0x4b00f210ca106ad0ull, + 0x889002201506274dull, 0x8150454982a85a00ull, 0x2c08880480002004ull, 0x4ac48001000508d1ull, + 0x0a42008e0062e0a0ull, 0xe0a5090e6a8c3055ull, 0x80b3481442c42906ull, 0x733c0102b330803eull, + 0x09400c20700d1494ull, 0xc094a451c040301aull, 0xa40c96c205c88dcaull, 0x011000c834040001ull, + 0x1cda2428a9cd550dull, 0x120f7a4d48370142ull, 0xd20531fb452a32b4ull, 0x45ca68d7dc44b894ull, + 0x420819432ed15097ull, 0xa09798409d48d202ull, 0x00000000064d5409ull, 0x0000000000000000ull, + 0x8480000000000000ull, 0x17001c0604215542ull, 0xb9ddff8761107624ull, 0x3c11245d5c0a659full, + 0x00000000005dadb0ull, 0x00db28d000000000ull, 0x4408010802000422ull, 0x90288d0aac409804ull, + 0x00310400e0018700ull, 0x1054001982211794ull, 0x40039c02021a2cb2ull, 0x7900080c8804bd60ull, + 0xcb088640ba3c1628ull, 0x0000001e90807274ull, 0x9c87e188d8000000ull, 0x2791ae6404124034ull, + 0x5366408fe6fbe86bull, 0xb5e4e3ab537feea6ull, 0x012285480002869full, 0x20a0211648004402ull, + 0x0005208002240004ull, 0x01ac162c01547e00ull, 0x05308c1410852a84ull, 0x906060fafdc3fbc3ull, + 0x9690120040336440ull, 0x418200d44e834b31ull, 0x028020801d6a0129ull, 0x9f0c269102ad8000ull, + 0x0c24d96f67018044ull, 0x5021500118d02910ull, 0x0201709004d01000ull, 0x0100013261c30148ull, + 0x0562080207190088ull, 0xf0a104054c0e0132ull, 0x0000000000000002ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0080000000000000ull, 0x5a0421bd035e8e8dull, 0x0000002611703488ull, + 0x8804c50210000000ull, 0x25ed147cf801b815ull, 0x1bd785893bb0ed60ull, 0x0ac50d0c1a627af3ull, + 0x6b0d0490524ae5d1ull, 0x16122b575266a35cull, 0x001829491101a872ull, 0x886c600010080948ull, + 0x39903012058f916eull, 0x001b88a049b0f840ull, 0x0042850000000000ull, 0x7014ea0498000058ull, + 0x60005193611d1628ull, 0x0000000000a71a24ull, 0x1018712043c00000ull, 0x89066004a9270172ull, + 0x40810900020cc022ull, 0x00000e348ca0602dull, 0x1101210000000000ull, 0x0892ec4cd31a8011ull, + 0x1806c7ac85000040ull, 0x003480000512e03eull, 0x0a126d0180cec008ull, 0x0027011e08568641ull, + 0x4e05e032083d3751ull, 0x01400081048401c0ull, 0x0000000000000000ull, 0x00591aa000000000ull, + 0xc8001d48882443c8ull, 0x0405981372030152ull, 0x0d148a1004008280ull, 0x2704a04002088056ull, + 0x000000004e000000ull, 0xa320000000000000ull, 0xdf002660a0ae1902ull, 0x3ad081217b17f010ull, + 0x4800100300284180ull, 0x00c414cf8014cc00ull, 0x0000000130202000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000020000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000010000000ull, + 0x00003fffffffc000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull, + 0xfffffffffffffffeull, 0xfffffffe7fffffffull, 0x00000000ffffffffull, 0x0000003f00000000ull, +}; + +struct CodepageRange { + std::uint16_t first; + std::uint16_t last; +}; + +inline constexpr CodepageRange kCodepageBRanges[151] = { + {0x0000, 0x00ff}, {0x0101, 0x0101}, {0x0113, 0x0113}, {0x011b, 0x011b}, {0x012b, 0x012b}, {0x0144, 0x0144}, + {0x0148, 0x0148}, {0x014d, 0x014d}, {0x016b, 0x016b}, {0x01ce, 0x01ce}, {0x01d0, 0x01d0}, {0x01d2, 0x01d2}, + {0x01d4, 0x01d4}, {0x01d6, 0x01d6}, {0x01d8, 0x01d8}, {0x01da, 0x01da}, {0x01dc, 0x01dc}, {0x01f9, 0x01f9}, + {0x0251, 0x0251}, {0x0261, 0x0261}, {0x02c7, 0x02c7}, {0x02c9, 0x02cb}, {0x02d9, 0x02d9}, {0x0391, 0x03a1}, + {0x03a3, 0x03a9}, {0x03b1, 0x03c1}, {0x03c3, 0x03c9}, {0x0401, 0x0401}, {0x0410, 0x044f}, {0x0451, 0x0451}, + {0x2010, 0x2010}, {0x2013, 0x2016}, {0x2018, 0x2019}, {0x201c, 0x201d}, {0x2025, 0x2026}, {0x2030, 0x2030}, + {0x2032, 0x2033}, {0x2035, 0x2035}, {0x203b, 0x203b}, {0x20ac, 0x20ac}, {0x2103, 0x2103}, {0x2105, 0x2105}, + {0x2109, 0x2109}, {0x2116, 0x2116}, {0x2121, 0x2121}, {0x2160, 0x216b}, {0x2170, 0x2179}, {0x2190, 0x2193}, + {0x2196, 0x2199}, {0x2208, 0x2208}, {0x220f, 0x220f}, {0x2211, 0x2211}, {0x2215, 0x2215}, {0x221a, 0x221a}, + {0x221d, 0x2220}, {0x2223, 0x2223}, {0x2225, 0x2225}, {0x2227, 0x222b}, {0x222e, 0x222e}, {0x2234, 0x2237}, + {0x223d, 0x223d}, {0x2248, 0x2248}, {0x224c, 0x224c}, {0x2252, 0x2252}, {0x2260, 0x2261}, {0x2264, 0x2267}, + {0x226e, 0x226f}, {0x2295, 0x2295}, {0x2299, 0x2299}, {0x22a5, 0x22a5}, {0x22bf, 0x22bf}, {0x2312, 0x2312}, + {0x2460, 0x2469}, {0x2474, 0x249b}, {0x2500, 0x254b}, {0x2550, 0x2573}, {0x2581, 0x258f}, {0x2593, 0x2595}, + {0x25a0, 0x25a1}, {0x25b2, 0x25b3}, {0x25bc, 0x25bd}, {0x25c6, 0x25c7}, {0x25cb, 0x25cb}, {0x25ce, 0x25cf}, + {0x25e2, 0x25e5}, {0x2605, 0x2606}, {0x2609, 0x2609}, {0x2640, 0x2640}, {0x2642, 0x2642}, {0x2e84, 0x2e84}, + {0x2e88, 0x2e88}, {0x2e8b, 0x2e8c}, {0x2e97, 0x2e97}, {0x2ea7, 0x2ea7}, {0x2eaa, 0x2eaa}, {0x2eb6, 0x2eb7}, + {0x2ebb, 0x2ebb}, {0x2ff0, 0x2ffb}, {0x3000, 0x3003}, {0x3005, 0x3017}, {0x301d, 0x301e}, {0x3021, 0x3029}, + {0x303e, 0x303e}, {0x3041, 0x3093}, {0x309b, 0x309e}, {0x30a1, 0x30f6}, {0x30fc, 0x30fe}, {0x3105, 0x3129}, + {0x3220, 0x3229}, {0x3231, 0x3231}, {0x32a3, 0x32a3}, {0x338e, 0x338f}, {0x339c, 0x339e}, {0x33a1, 0x33a1}, + {0x33c4, 0x33c4}, {0x33ce, 0x33ce}, {0x33d1, 0x33d2}, {0x33d5, 0x33d5}, {0x3400, 0x3917}, {0x3919, 0x39ce}, + {0x39d0, 0x39de}, {0x39e0, 0x415e}, {0x4160, 0x4336}, {0x4338, 0x4728}, {0x472a, 0x477b}, {0x477d, 0x478c}, + {0x478e, 0x4c9e}, {0x4ca1, 0x4ca2}, {0x4ca4, 0x4db5}, {0x4e00, 0x9fa5}, {0xe766, 0xe864}, {0xf92c, 0xf92c}, + {0xf979, 0xf979}, {0xf995, 0xf995}, {0xf9e7, 0xf9e7}, {0xf9f1, 0xf9f1}, {0xfa0c, 0xfa0f}, {0xfa11, 0xfa11}, + {0xfa13, 0xfa14}, {0xfa18, 0xfa18}, {0xfa1f, 0xfa21}, {0xfa23, 0xfa24}, {0xfa27, 0xfa29}, {0xfe30, 0xfe31}, + {0xfe33, 0xfe44}, {0xfe49, 0xfe52}, {0xfe54, 0xfe57}, {0xfe59, 0xfe66}, {0xfe68, 0xfe6b}, {0xff01, 0xff5e}, + {0xffe0, 0xffe5}, +}; + +} diff --git a/core/libs/prx/libSceIme/include/ImeFilterText.hpp b/core/libs/prx/libSceIme/include/ImeFilterText.hpp new file mode 100644 index 0000000000..6c465fa07e --- /dev/null +++ b/core/libs/prx/libSceIme/include/ImeFilterText.hpp @@ -0,0 +1,177 @@ +#pragma once + +#include +#include + +#include "ImeFilterCodepages.hpp" + +namespace ime_filter { + +struct FilterParam { + std::uint64_t mask; + std::uint32_t type; + std::uint32_t version; + std::uint32_t variant; +}; + +static_assert(sizeof(FilterParam) == 24); + +constexpr std::uint32_t kErrorInvalidParam = 0x80bc0030u; +constexpr std::uint32_t kErrorInvalidAddress = 0x80bc0031u; + +constexpr std::uint64_t kClassCyrillic = 0x200000400ull; +constexpr std::uint64_t kClassVietnamese = 0x40000000ull; +constexpr std::uint64_t kClassRomanian = 0x80000000ull; +constexpr std::uint64_t kClassThai = 0x4000000ull; + +constexpr std::uint64_t kAbsentMasks[8] = {0x188000000ull, 0x40000000ull, 0x10000000ull, 0x1000000ull, + 0x2000ull, 0x18000ull, 0x6000ull, 0x4000ull}; + +constexpr char16_t kPunctuationLatin[] = { + 0xa1, 0xbf, 0xb4, 0x2018, 0x2019, 0x201a, 0x201c, 0x201d, 0x201e, 0xb0, 0x2039, 0x203a, + 0xab, 0xbb, 0xaa, 0xba, 0xd7, 0xf7, 0xa4, 0xa2, 0x20ac, 0xa3, 0xa5, 0x20a9, + 0xa7, 0xa6, 0xb5, 0xac, 0xb7, 0xb9, 0xb2, 0xb3, 0xbc, 0xbd, 0xbe, 0x2116}; + +constexpr char16_t kPunctuationFullwidth[] = { + 0x3001, 0x3002, 0x30fc, 0x30fb, 0x300c, 0x300d, 0xffe5, 0xff01, 0x201d, 0xff03, 0xff04, 0xff05, 0xff06, + 0x2019, 0xff08, 0xff09, 0xff0a, 0xff0b, 0xff0c, 0xff0d, 0xff0e, 0xff0f, 0xff1a, 0xff1b, 0xff1c, 0xff1d, + 0xff1e, 0xff1f, 0xff20, 0xff3b, 0xff3c, 0xff3d, 0xff3e, 0xff3f, 0xff40, 0xff5b, 0xff5c, 0xff5d, 0xff5e}; + +constexpr char16_t kPunctuationFullwidthNoProlong[] = { + 0x3001, 0x3002, 0x300c, 0x300d, 0xff01, 0x201d, 0xff03, 0xff04, 0xff05, 0xff06, 0x2019, 0xff08, + 0xff09, 0xff0a, 0xff0b, 0xff0c, 0xff0d, 0xff0e, 0xff0f, 0xff1a, 0xff1b, 0xff1c, 0xff1d, 0xff1e, + 0xff1f, 0xff20, 0xff3b, 0xff3c, 0xff3d, 0xff3e, 0xff3f, 0xff40, 0xff5b, 0xff5c, 0xff5d, 0xff5e}; + +constexpr char16_t kVietnameseLetters[] = {0x1a0, 0x1a1, 0x1af, 0x1b0, 0x20ab}; +constexpr char16_t kModifierLetters[] = {0x2c7, 0x2d8, 0x2d9, 0x2db, 0x2dd}; +constexpr char16_t kNumberSeparators[] = {0x2c, 0x2e, 0x2d, 0x0a}; + +template +constexpr bool In(const char16_t (&set)[N], std::uint32_t c) { + for (char16_t v : set) { + if (v == c) return true; + } + return false; +} + +constexpr bool InCodepageA(std::uint32_t c) { + return ((kCodepageABitmap[c >> 6] >> (c & 63)) & 1u) != 0; +} + +constexpr bool InCodepageB(std::uint32_t c) { + constexpr std::size_t count = sizeof(kCodepageBRanges) / sizeof(kCodepageBRanges[0]); + std::size_t low = 0; + std::size_t high = count; + while (low < high) { + const std::size_t middle = (low + high) / 2; + if (c > kCodepageBRanges[middle].last) { + low = middle + 1; + } else { + high = middle; + } + } + return low < count && c >= kCodepageBRanges[low].first; +} + +constexpr bool IsVietnameseExtension(std::uint32_t c) { + return static_cast(c - 0x300) < 0x24 || (c & 0xff00) == 0x1e00 || In(kVietnameseLetters, c); +} + +constexpr std::uint64_t DerivedMask(std::uint32_t version, std::uint32_t variant) { + const std::uint32_t second = variant == 1 ? 0x10000000u : 0x6000000u; + const std::uint64_t flag = (variant == 0 || version >= 0x2000000u) ? (1u << 24) : 0; + std::uint64_t low = flag + 0x1fffffu; + const std::uint64_t high = flag | 0x21fffffu; + if (version >= 0x2500000u || variant == 0) low = high; + const std::uint64_t wide = 0x200000000ull + low - 0x4000000ull; + if (version >= 0x5000000u || variant == 0) low = wide; + return version < second ? low : (low | 0x200000000ull); +} + +constexpr bool ValidMask(std::uint64_t mask, std::uint32_t version, std::uint32_t variant, std::uint32_t* limit) { + const bool none = variant == 0; + const bool wide = none || version >= 0x5000000u; + const std::uint64_t shifted = (none || version >= 0x2500000u ? 1u : 0u) << 25; + const std::uint64_t first = wide ? 0x200000000ull + shifted - 0x17e1e401ull : (shifted | 0x1e1bffull); + if ((mask & first) != 0) { + *limit = 0x17f; + return true; + } + *limit = 0x7f; + const std::uint32_t second = variant == 1 ? 0x10000000u : 0x6000000u; + const std::uint64_t flag = static_cast(none || version >= 0x2000000u ? 1u : 0u) << 24; + const std::uint64_t base = (wide ? 0x1401e400ull : 0x1e400ull) | flag; + const std::uint64_t allowed = version < second ? base : (base | 0x200000000ull); + return (mask & allowed) != 0; +} + +constexpr bool HighBitSet(std::uint64_t mask) { + return (mask & 0x80000000ull) != 0; +} + +constexpr bool KeepClassified(std::uint32_t c, std::uint64_t mask, std::uint32_t limit, const bool (&absent)[8]) { + if (c < limit) return true; + if (In(kPunctuationLatin, c)) return true; + if (!absent[7] && (static_cast(c - 0x3130) < 0x60 || static_cast(c + 0x5400) < 0x2bb0)) { + return true; + } + if ((kClassCyrillic & mask) != 0 && (c & 0xff00) == 0x400) return true; + if (!absent[6] && (static_cast(c + 0xf0) <= 9 || In(kPunctuationFullwidth, c))) return true; + if (!absent[5] && (static_cast(c + 0xf0) <= 9 || In(kPunctuationFullwidthNoProlong, c))) return true; + if (!absent[4] && InCodepageA(c)) return true; + if (!absent[5] && InCodepageB(c)) return true; + if (!absent[3] && ((c & 0xff00) == 0x600 || (c & 0xfffc) == 0x200c)) return true; + if ((mask & kClassThai) != 0 && (c & 0xff80) == 0xe00) return true; + if (!absent[2] && (static_cast(c - 0x370) < 0x72 || (c & 0xfff0) == 0x3f0)) return true; + if (!absent[1] && IsVietnameseExtension(c)) return true; + if (HighBitSet(mask) && (c & 0xfffc) == 0x218) return true; + if (!absent[0] && In(kModifierLetters, c)) return true; + return false; +} + +inline std::uint32_t FilterText(char16_t* dst, std::int32_t* out_length, std::uint32_t capacity, + const FilterParam* param, const char16_t* src, std::uint32_t src_length) { + if (dst == nullptr || out_length == nullptr || param == nullptr || src == nullptr) return kErrorInvalidAddress; + if (capacity < 2) return kErrorInvalidParam; + const std::uint32_t type = param->type; + if (type >= 5 && type - 0x100 > 3) return kErrorInvalidParam; + + std::uint64_t mask = param->mask; + if (mask == 0) mask = DerivedMask(param->version, param->variant); + std::uint32_t limit = 0; + if (!ValidMask(mask, param->version, param->variant, &limit)) return kErrorInvalidParam; + + bool absent[8]; + for (int i = 0; i < 8; ++i) absent[i] = (mask & kAbsentMasks[i]) == 0; + + std::uint32_t room = capacity - 1; + std::uint32_t count = 0; + for (std::uint64_t i = 0; i < src_length && room != 0; ++i) { + const std::uint32_t c = src[i]; + if (c == 0) break; + bool keep; + if (type == 4) { + keep = static_cast(c - 0x30) <= 9 || In(kNumberSeparators, c); + } else if (type == 1 || type == 3 || type == 0x102) { + keep = c <= 0x7e; + } else if (type == 0x100) { + keep = c < 0x17f; + if (!keep && (mask & kClassVietnamese) != 0) { + const bool listed = static_cast(c - 0x300) >= 0x24 && (c & 0xff00) != 0x1e00; + keep = !listed || In(kVietnameseLetters, c); + } + if (!keep && HighBitSet(mask)) keep = (c & 0xfffc) == 0x218; + } else { + keep = KeepClassified(c, mask, limit, absent); + } + if (keep) { + dst[count++] = static_cast(c); + --room; + } + } + dst[count] = 0; + *out_length = static_cast(count); + return 0; +} + +} diff --git a/core/libs/prx/libSceImeDialog/Export.cpp b/core/libs/prx/libSceImeDialog/Export.cpp index 71df834abb..c9b181b025 100644 --- a/core/libs/prx/libSceImeDialog/Export.cpp +++ b/core/libs/prx/libSceImeDialog/Export.cpp @@ -18,9 +18,11 @@ constexpr std::uint32_t EndStatusAborted = 2; constexpr std::uint32_t MaxTextLength = 2048; constexpr int ErrorBusy = static_cast(0x80bc0001u); constexpr int ErrorNotOpened = static_cast(0x80bc0002u); +constexpr int ErrorNotInUse = static_cast(0x80bc0107u); constexpr int ErrorInvalidMaxTextLength = static_cast(0x80bc0016u); constexpr int ErrorInvalidInputTextBuffer = static_cast(0x80bc0017u); constexpr int ErrorInvalidAddress = static_cast(0x80bc0031u); +constexpr int ErrorInternal = static_cast(0x80bc00ffu); struct Dialog { int status = StatusNone; @@ -175,4 +177,37 @@ int APS5_VABI sceImeDialogTerm(void) { return 0; } +int APS5_VABI sceImeDialogForTestFunction() { + return ErrorInternal; +} + +int APS5_VABI sceImeDialogForceClose(void) { + std::lock_guard lock(g_mutex); + if (g_dialog.status == StatusNone) return ErrorNotInUse; + ImeSetHostTextSink_nid_no_patch(nullptr); + g_dialog = Dialog{}; + 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; +} + + +int APS5_VABI sceImeDialogInitInternalWithSubInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceIpmi/Export.cpp b/core/libs/prx/libSceIpmi/Export.cpp index fb34206c21..238bb67ecf 100644 --- a/core/libs/prx/libSceIpmi/Export.cpp +++ b/core/libs/prx/libSceIpmi/Export.cpp @@ -6,5 +6,29 @@ extern "C" { APS5_DUMMY_FUN +int APS5_VABI sceIpmiMgrGetIpcBlockerPid(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceIpmiMgrGetIpcBlockerPidByAppId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceIpmiMgrPauseDump(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceIpmiMgrRaiseExceptionToPid(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceIpmiMgrStartDump(void) { + 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..19ec61c18b 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) { @@ -105,22 +111,25 @@ int32_t APS5_VABI sceJpegDecDecode(void* handle, const JpegDecDecodeParam* param if (!context) return SCE_JPEG_DEC_ERROR_INVALID_HANDLE; (void)context; if (!param) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; - if (!param->jpeg_mem_addr || !param->image_mem_addr) return SCE_JPEG_DEC_ERROR_INVALID_ADDR; + if (!param->jpeg_mem_addr || !param->image_mem_addr || (reinterpret_cast(param->image_mem_addr) & 3u) != 0) 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); + if (param->alpha_value > 0xFF) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; + 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 +153,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 +169,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 +182,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/CMakeLists.txt b/core/libs/prx/libSceJson2/CMakeLists.txt index dff923fb2f..f3e7cb682f 100644 --- a/core/libs/prx/libSceJson2/CMakeLists.txt +++ b/core/libs/prx/libSceJson2/CMakeLists.txt @@ -13,3 +13,18 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +if(BUILD_TESTING) + add_test_executable(guest_json_copy_failure_tests ${LIBS_INCLUDE_DIR}/tests/JsonCopyFailure.cpp Export.cpp) + target_include_directories(guest_json_copy_failure_tests PRIVATE ${LIBS_INCLUDE_DIR}) + if(WIN32) + target_link_libraries(guest_json_copy_failure_tests PRIVATE libc) + configure_windows_unwind(guest_json_copy_failure_tests) + endif() + add_test(NAME guest_json_copy_failure COMMAND guest_json_copy_failure_tests) + set_tests_properties(guest_json_copy_failure PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_json_copy_failure PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceJson2/Export.cpp b/core/libs/prx/libSceJson2/Export.cpp index 584f2c7957..6231181390 100644 --- a/core/libs/prx/libSceJson2/Export.cpp +++ b/core/libs/prx/libSceJson2/Export.cpp @@ -128,23 +128,47 @@ Node& NodeOf(const Value& v) { void Construct(Value& v) { v.node = new Node(); } -void CopyArray(Array& destination, const Array& source) { - Construct(destination); - for (const auto& item : *source.items) { - Value copy{}; - CopyInto(copy, item); - destination.items->push_back(copy); +template +void Construct(Value& destination, TInitialize initialize) { + Value pending{}; + Construct(pending); + try { + initialize(*pending.node); + } catch (...) { + Destroy(pending); + throw; } + destination = pending; +} + +void CopyArray(Array& destination, const Array& source) { + Array pending{}; + Construct(pending); + try { + for (const auto& item : *source.items) { + CopyInto(pending.items->emplace_back(), item); + } + } catch (...) { + Destroy(pending); + throw; + } + destination = pending; } void CopyObject(Object& destination, const Object& source) { - Construct(destination); - for (const auto& pair : *source.items) { - Pair copy{}; - Construct(copy.key, *pair.key.text); - CopyInto(copy.value, pair.value); - destination.items->push_back(copy); + Object pending{}; + Construct(pending); + try { + for (const auto& pair : *source.items) { + auto& copy = pending.items->emplace_back(); + Construct(copy.key, *pair.key.text); + CopyInto(copy.value, pair.value); + } + } catch (...) { + Destroy(pending); + throw; } + destination = pending; } void AssignNode(Node& destination, const Node& source) { @@ -154,26 +178,33 @@ void AssignNode(Node& destination, const Node& source) { copy.integer = source.integer; copy.uinteger = source.uinteger; copy.real = source.real; - if (source.type == TypeString) Construct(copy.string, *source.string.text); - if (source.type == TypeArray) CopyArray(copy.array, source.array); - if (source.type == TypeObject) CopyObject(copy.object, source.object); + try { + if (source.type == TypeString) Construct(copy.string, *source.string.text); + if (source.type == TypeArray) CopyArray(copy.array, source.array); + if (source.type == TypeObject) CopyObject(copy.object, source.object); + } catch (...) { + Clear(copy); + throw; + } Clear(destination); copy.handlers = destination.handlers; destination = copy; } void CopyInto(Value& destination, const Value& source) { - Construct(destination); - AssignNode(*destination.node, NodeOf(source)); + Construct(destination, [&](Node& node) { AssignNode(node, NodeOf(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) { @@ -576,8 +607,12 @@ String* APS5_VABI _ZN3sce4Json6StringaSERKS1_(String* self, const String* other) const char* APS5_VABI _ZNK3sce4Json6String5c_strEv(const String* self) { return self->text->c_str(); } bool APS5_VABI _ZNK3sce4Json6String5emptyEv(const String* self) { return self->text->empty(); } std::size_t APS5_VABI _ZNK3sce4Json6String6lengthEv(const String* self) { return self->text->size(); } +std::size_t APS5_VABI _ZNK3sce4Json6String4sizeEv(const String* self) { return self->text->size(); } +std::size_t APS5_VABI _ZNK3sce4Json6String8capacityEv(const String* self) { return self->text->capacity(); } bool APS5_VABI _ZNK3sce4Json6StringeqEPKc(const String* self, const char* text) { return text != nullptr && *self->text == text; } +bool APS5_VABI _ZNK3sce4Json6StringeqERKS1_(const String* self, const String* other) { return other != nullptr && other->text != nullptr && *self->text == *other->text; } void APS5_VABI _ZN3sce4Json6String5clearEv(String* self) { self->text->clear(); } +void APS5_VABI _ZN3sce4Json6String6resizeEm(String* self, std::size_t count) { self->text->resize(count); } void APS5_VABI _ZN3sce4Json6String7reserveEm(String* self, std::size_t capacity) { self->text->reserve(capacity); } String* APS5_VABI _ZN3sce4Json6String6appendERKS1_(String* self, const String* other) { const std::string copy = *other->text; @@ -589,6 +624,18 @@ String* APS5_VABI _ZN3sce4Json6String6appendEPKc(String* self, const char* text) self->text->append(text); return self; } +String* APS5_VABI _ZN3sce4Json6String6appendEPKcm(String* self, const char* text, std::size_t length) { + if (text == nullptr && length > 0) throw std::invalid_argument("sce::Json::String::append: null text"); + self->text->append(text, length); + return self; +} +String* APS5_VABI _ZN3sce4Json6StringpLEPKc(String* self, const char* text) { + return _ZN3sce4Json6String6appendEPKc(self, text); +} +String* APS5_VABI _ZN3sce4Json6StringpLEh(String* self, unsigned char ch) { + self->text->push_back(static_cast(ch)); + return self; +} String* APS5_VABI _ZNK3sce4Json6String6substrEmm(String* result, const String* self, std::size_t position, std::size_t length) { if (position > self->text->size()) throw std::out_of_range("sce::Json::String::substr: position past the end"); Construct(*result, self->text->substr(position, length)); @@ -600,6 +647,10 @@ int APS5_VABI _ZNK3sce4Json6String7compareEPKc(const String* self, const char* t if (text == nullptr) throw std::invalid_argument("sce::Json::String::compare: null text"); return self->text->compare(text); } +int APS5_VABI _ZNK3sce4Json6String7compareERKS1_(const String* self, const String* other) { + if (other == nullptr || other->text == nullptr) throw std::invalid_argument("sce::Json::String::compare: null other"); + return self->text->compare(*other->text); +} void APS5_VABI _ZN3sce4Json5ArrayC1Ev(Array* self) { Construct(*self); } void APS5_VABI _ZN3sce4Json5ArrayC1ERKS1_(Array* self, const Array* other) { CopyArray(*self, *other); } @@ -607,7 +658,12 @@ void APS5_VABI _ZN3sce4Json5ArrayD1Ev(Array* self) { Destroy(*self); } void APS5_VABI _ZN3sce4Json5Array9push_backERKNS0_5ValueE(Array* self, const Value* value) { Value copy{}; CopyInto(copy, *value); - self->items->push_back(copy); + try { + self->items->push_back(copy); + } catch (...) { + Destroy(copy); + throw; + } } ArrayIterator* APS5_VABI _ZNK3sce4Json5Array5beginEv(ArrayIterator* result, const Array* self) { result->it = self->items->begin(); return result; } ArrayIterator* APS5_VABI _ZNK3sce4Json5Array3endEv(ArrayIterator* result, const Array* self) { result->it = self->items->end(); return result; } @@ -630,6 +686,11 @@ ArrayIterator* APS5_VABI _ZN3sce4Json5Array8iteratorppEv(ArrayIterator* self) { Value* APS5_VABI _ZNK3sce4Json5Array8iteratordeEv(const ArrayIterator* self) { return &*self->it; } Value* APS5_VABI _ZNK3sce4Json5Array8iteratorptEv(const ArrayIterator* self) { return &*self->it; } bool APS5_VABI _ZNK3sce4Json5Array8iteratorneERKS2_(const ArrayIterator* self, const ArrayIterator* other) { return self->it != other->it; } +void APS5_VABI _ZN3sce4Json5Array8iteratorC1ERKS2_(ArrayIterator* self, const ArrayIterator* other) { self->it = other->it; } +ArrayIterator* APS5_VABI _ZN3sce4Json5Array8iterator7advanceEm(ArrayIterator* self, std::size_t count) { + std::advance(self->it, count); + return self; +} void APS5_VABI _ZN3sce4Json6ObjectC1Ev(Object* self) { Construct(*self); } void APS5_VABI _ZN3sce4Json6ObjectC1ERKS1_(Object* self, const Object* other) { CopyObject(*self, *other); } @@ -661,8 +722,23 @@ ObjectIterator* APS5_VABI _ZNK3sce4Json6Object4findERKNS0_6StringE(ObjectIterato return result; } bool APS5_VABI _ZNK3sce4Json6Object8iteratorneERKS2_(const ObjectIterator* self, const ObjectIterator* other) { return self->it != other->it; } +bool APS5_VABI _ZNK3sce4Json6Object8iteratoreqERKS2_(const ObjectIterator* self, const ObjectIterator* other) { return self->it == other->it; } +void APS5_VABI _ZN3sce4Json6Object8iteratorC1ERKS2_(ObjectIterator* self, const ObjectIterator* other) { self->it = other->it; } +ObjectIterator* APS5_VABI _ZN3sce4Json6Object8iterator7advanceEm(ObjectIterator* self, std::size_t count) { + std::advance(self->it, count); + return self; +} bool APS5_VABI _ZNK3sce4Json6Object5emptyEv(const Object* self) { return self->items->empty(); } std::size_t APS5_VABI _ZNK3sce4Json6Object4sizeEv(const Object* self) { return self->items->size(); } +std::size_t APS5_VABI _ZN3sce4Json6Object5eraseERKNS0_6StringE(Object* self, const String* key) { + if (key == nullptr || key->text == nullptr) throw std::invalid_argument("sce::Json::Object::erase: null key"); + auto it = std::find_if(self->items->begin(), self->items->end(), [&](const Pair& pair) { return *pair.key.text == *key->text; }); + if (it == self->items->end()) return 0; + Destroy(it->key); + Destroy(it->value); + self->items->erase(it); + return 1; +} void APS5_VABI _ZN3sce4Json5ValueC1Ev(Value* self) { Construct(*self); } void APS5_VABI _ZN3sce4Json5ValueC1Eb(Value* self, bool value) { Construct(*self); self->node->type = TypeBoolean; self->node->boolean = value; } @@ -671,24 +747,28 @@ void APS5_VABI _ZN3sce4Json5ValueC1Em(Value* self, std::uint64_t value) { Constr void APS5_VABI _ZN3sce4Json5ValueC1Ed(Value* self, double value) { Construct(*self); self->node->type = TypeReal; self->node->real = value; } void APS5_VABI _ZN3sce4Json5ValueC1EPKc(Value* self, const char* text) { if (text == nullptr) throw std::invalid_argument("sce::Json::Value: null text"); - Construct(*self); - SetType(*self->node, TypeString); - *self->node->string.text = text; + Construct(*self, [text](Node& node) { + SetType(node, TypeString); + *node.string.text = text; + }); } void APS5_VABI _ZN3sce4Json5ValueC1ERKNS0_6StringE(Value* self, const String* text) { - Construct(*self); - SetType(*self->node, TypeString); - *self->node->string.text = *text->text; + Construct(*self, [text](Node& node) { + SetType(node, TypeString); + *node.string.text = *text->text; + }); } void APS5_VABI _ZN3sce4Json5ValueC1ERKNS0_5ArrayE(Value* self, const Array* array) { - Construct(*self); - self->node->type = TypeArray; - CopyArray(self->node->array, *array); + Construct(*self, [array](Node& node) { + node.type = TypeArray; + CopyArray(node.array, *array); + }); } void APS5_VABI _ZN3sce4Json5ValueC1ERKNS0_6ObjectE(Value* self, const Object* object) { - Construct(*self); - self->node->type = TypeObject; - CopyObject(self->node->object, *object); + Construct(*self, [object](Node& node) { + node.type = TypeObject; + CopyObject(node.object, *object); + }); } void APS5_VABI _ZN3sce4Json5ValueC1ERKS1_(Value* self, const Value* other) { CopyInto(*self, *other); } void APS5_VABI _ZN3sce4Json5ValueD1Ev(Value* self) { Destroy(*self); } @@ -696,6 +776,9 @@ Value* APS5_VABI _ZN3sce4Json5ValueaSERKS1_(Value* self, const Value* other) { if (self != other) AssignNode(NodeOf(*self), NodeOf(*other)); return self; } +void APS5_VABI _ZN3sce4Json5Value4swapERS1_(Value* self, Value* other) { + std::swap(self->node, other->node); +} int APS5_VABI _ZN3sce4Json5Value3setENS0_9ValueTypeE(Value* self, ValueType type) { SetType(NodeOf(*self), type); return 0; } int APS5_VABI _ZN3sce4Json5Value3setEb(Value* self, bool value) { SetType(NodeOf(*self), TypeBoolean); self->node->boolean = value; return 0; } @@ -788,8 +871,7 @@ const Value* APS5_VABI _ZNK3sce4Json5ValueixEm(const Value* self, std::size_t in return &MissingElement(self); } void APS5_VABI _ZN3sce4Json5ValueC1ENS0_9ValueTypeE(Value* self, ValueType type) { - Construct(*self); - SetType(*self->node, type); + Construct(*self, [type](Node& node) { SetType(node, type); }); } const Value* APS5_VABI _ZNK3sce4Json5Value8getValueEm(const Value* self, std::size_t index) { return _ZNK3sce4Json5ValueixEm(self, index); } const Value* APS5_VABI _ZNK3sce4Json5Value8getValueERKNS0_6StringE(const Value* self, const String* key) { @@ -973,13 +1055,16 @@ int APS5_VABI _ZN3sce4Json5Value22setTypeMismatchHandlerEPFRKS1_NS0_9ValueTypeEP return 0; } -int APS5_VABI _ZNK3sce4Json5ValuecvbEv(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +bool APS5_VABI _ZNK3sce4Json5ValuecvbEv(const Value* self) { + const Node& n = NodeOf(*self); + if (n.type == TypeNull) return false; + if (n.type == TypeBoolean) return n.boolean; + return true; } void APS5_VABI _ZN3sce4Json6Object4PairC1ERKNS0_6StringERKNS0_5ValueE(Pair* self, const String* key, const Value* value) { Pair pair{}; + pair.reserved = 0; Construct(pair.key, *key->text); try { CopyInto(pair.value, *value); @@ -995,9 +1080,25 @@ void APS5_VABI _ZN3sce4Json6Object4PairD1Ev(Pair* self) { Destroy(self->value); } -int APS5_VABI _ZN3sce4Json6Object6insertERKNS1_4PairE() { - NotImplemented_nid_no_patch(__func__); - return 0; +ObjectIterator* APS5_VABI _ZN3sce4Json6Object6insertERKNS1_4PairE(ObjectIterator* result, Object* self, const Pair* pair) { + auto it = std::find_if(self->items->begin(), self->items->end(), [&](const Pair& p) { return *p.key.text == *pair->key.text; }); + if (it != self->items->end()) { + AssignNode(NodeOf(it->value), NodeOf(pair->value)); + result->it = it; + return result; + } + Pair copy{}; + copy.reserved = 0; + Construct(copy.key, *pair->key.text); + try { + CopyInto(copy.value, pair->value); + result->it = self->items->insert(self->items->end(), copy); + return result; + } catch (...) { + Destroy(copy.value); + Destroy(copy.key); + throw; + } } ArrayIterator* APS5_VABI _ZN3sce4Json5Array8iteratorppEi(ArrayIterator* result, ArrayIterator* self, int) { @@ -1014,4 +1115,32 @@ ArrayIterator* APS5_VABI _ZN3sce4Json5Array8iteratoraSERKS2_(ArrayIterator* self void APS5_VABI _ZN3sce4Json5Array8iteratorC1Ev(ArrayIterator* self) { self->it = {}; } + +ObjectIterator* APS5_VABI _ZN3sce4Json6Object8iteratorppEi(ObjectIterator* result, ObjectIterator* self, int) { + result->it = self->it; + ++self->it; + return result; +} + +ObjectIterator* APS5_VABI _ZN3sce4Json6Object8iteratoraSERKS2_(ObjectIterator* self, const ObjectIterator* other) { + self->it = other->it; + return self; +} + +void APS5_VABI _ZN3sce4Json6Object8iteratorC1Ev(ObjectIterator* self) { + self->it = {}; +} + +void APS5_VABI _ZN3sce4Json6Object4PairC1Ev(Pair* self) { + Pair pair{}; + pair.reserved = 0; + Construct(pair.key, std::string()); + try { + Construct(pair.value); + } catch (...) { + Destroy(pair.key); + throw; + } + *self = pair; +} } diff --git a/core/libs/prx/libSceJxr/Export.cpp b/core/libs/prx/libSceJxr/Export.cpp index d81f4a5e32..608aedc418 100644 --- a/core/libs/prx/libSceJxr/Export.cpp +++ b/core/libs/prx/libSceJxr/Export.cpp @@ -6,5 +6,24 @@ extern "C" { APS5_DUMMY_FUN +int APS5_VABI PKFree_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI PixelFormatLookup_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI readIS_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI writeIS_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} } 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/libSceLibreSsl/Export.cpp b/core/libs/prx/libSceLibreSsl/Export.cpp index d81f4a5e32..93b05e0fec 100644 --- a/core/libs/prx/libSceLibreSsl/Export.cpp +++ b/core/libs/prx/libSceLibreSsl/Export.cpp @@ -6,5 +6,1999 @@ extern "C" { APS5_DUMMY_FUN +int APS5_VABI AES_cbc_encrypt_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI AES_cfb128_encrypt_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI AES_ctr128_encrypt_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI AES_decrypt_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI AES_ecb_encrypt_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI AES_encrypt_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI AES_set_decrypt_key_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI AES_set_encrypt_key_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ASN1_INTEGER_get_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ASN1_STRING_data_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ASN1_STRING_length_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ASN1_STRING_print_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ASN1_STRING_to_UTF8_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ASN1_STRING_type_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ASN1_TIME_print_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BIO_ctrl_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BIO_f_ssl_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BIO_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BIO_new_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BIO_new_fp_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BIO_new_mem_buf_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BIO_new_socket_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BIO_printf_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BIO_puts_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BIO_s_file_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BIO_s_mem_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BIO_write_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BN_CTX_end_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BN_CTX_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BN_CTX_new_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BN_CTX_start_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BN_add_word_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BN_bin2bn_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BN_bn2bin_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BN_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BN_hex2bn_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BN_mod_inverse_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BN_mul_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BN_new_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BN_num_bits_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BN_print_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BN_set_word_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BN_sub_word_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI CMAC_CTX_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI CMAC_CTX_new_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI CMAC_Final_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI CMAC_Init_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI CMAC_Update_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI CONF_modules_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI CONF_modules_load_file_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI CRYPTO_THREADID_set_callback_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI CRYPTO_THREADID_set_numeric_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI CRYPTO_cleanup_all_ex_data_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI CRYPTO_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI CRYPTO_malloc_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI CRYPTO_num_locks_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI CRYPTO_set_add_lock_callback_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI CRYPTO_set_locking_callback_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI CRYPTO_set_mem_functions_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI DES_ecb_encrypt_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI DES_set_key_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI DES_set_odd_parity_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI DTLSv1_client_method_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI DTLSv1_server_method_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ECDH_compute_key_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ECDSA_sign_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ECDSA_size_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ECDSA_verify_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EC_GROUP_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EC_GROUP_get_degree_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EC_GROUP_new_by_curve_name_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EC_KEY_check_key_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EC_KEY_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EC_KEY_generate_key_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EC_KEY_get0_group_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EC_KEY_get0_private_key_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EC_KEY_get0_public_key_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EC_KEY_new_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EC_KEY_new_by_curve_name_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EC_KEY_set_group_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EC_KEY_set_private_key_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EC_KEY_set_public_key_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EC_POINT_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EC_POINT_mul_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EC_POINT_new_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EC_POINT_oct2point_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EC_POINT_point2oct_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ENGINE_by_id_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ENGINE_cleanup_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ENGINE_ctrl_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ENGINE_ctrl_cmd_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ENGINE_finish_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ENGINE_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ENGINE_get_first_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ENGINE_get_id_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ENGINE_get_next_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ENGINE_init_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ENGINE_load_builtin_engines_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ENGINE_load_private_key_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ENGINE_set_default_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ERR_clear_error_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ERR_error_string_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ERR_error_string_n_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ERR_free_strings_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ERR_get_error_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ERR_peek_error_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ERR_remove_thread_state_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_CIPHER_CTX_cleanup_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_CIPHER_CTX_ctrl_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_CIPHER_CTX_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_CIPHER_CTX_init_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_CIPHER_CTX_new_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_CIPHER_CTX_set_padding_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_CIPHER_block_size_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_CipherFinal_ex_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_CipherInit_ex_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_CipherUpdate_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_DecryptFinal_ex_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_DecryptInit_ex_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_DecryptUpdate_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_Digest_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_DigestFinal_ex_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_DigestInit_ex_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_DigestUpdate_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_DigestVerifyFinal_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_DigestVerifyInit_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_EncryptFinal_ex_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_EncryptInit_ex_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_EncryptUpdate_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_MD_CTX_cleanup_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_MD_CTX_create_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_MD_CTX_destroy_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_MD_CTX_init_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_MD_size_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_PKEY_copy_parameters_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_PKEY_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_PKEY_get1_EC_KEY_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_PKEY_get1_RSA_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_PKEY_id_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_PKEY_new_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_PKEY_set1_RSA_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_PKEY_type_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_aes_128_cbc_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_aes_128_cfb8_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_aes_128_ctr_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_aes_128_gcm_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_aes_192_cbc_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_aes_192_cfb8_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_aes_192_ctr_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_aes_192_gcm_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_aes_256_cbc_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_aes_256_cfb8_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_aes_256_ctr_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_aes_256_gcm_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_cleanup_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_md5_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_sha256_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_sha384_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_sha512_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI GENERAL_NAMES_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI GENERAL_NAME_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI HMAC_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI HMAC_CTX_cleanup_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI HMAC_CTX_init_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI HMAC_Final_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI HMAC_Init_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI HMAC_Init_ex_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI HMAC_Update_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI MD4_Final_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI MD4_Init_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI MD4_Update_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI MD5_Final_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI MD5_Init_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI MD5_Update_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OBJ_cleanup_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OBJ_create_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OBJ_nid2sn_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OBJ_obj2nid_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OBJ_sn2nid_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OCSP_BASICRESP_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OCSP_RESPONSE_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OCSP_basic_add1_cert_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OCSP_basic_verify_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OCSP_cert_status_str_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OCSP_check_validity_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OCSP_crl_reason_str_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OCSP_resp_count_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OCSP_resp_get0_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OCSP_response_get1_basic_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OCSP_response_status_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OCSP_response_status_str_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OCSP_single_get0_status_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OPENSSL_add_all_algorithms_noconf_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OPENSSL_load_builtin_modules_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI PEM_X509_INFO_read_bio_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI PEM_read_RSAPrivateKey_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI PEM_read_X509_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI PEM_read_bio_PrivateKey_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI PEM_read_bio_X509_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI PEM_read_bio_X509_AUX_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI PEM_write_bio_X509_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI PKCS12_PBE_add_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI PKCS12_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI PKCS12_parse_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI RAND_add_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI RAND_bytes_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI RAND_file_name_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI RAND_load_file_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI RAND_status_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI RSA_flags_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI RSA_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI RSA_generate_key_ex_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI RSA_new_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI RSA_padding_add_PKCS1_PSS_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI RSA_private_decrypt_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI RSA_private_encrypt_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI RSA_public_decrypt_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI RSA_public_encrypt_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI RSA_sign_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI RSA_size_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI RSA_verify_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI RSA_verify_PKCS1_PSS_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SHA1_Final_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SHA1_Init_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SHA1_Update_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SHA224_Final_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SHA224_Init_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SHA224_Update_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SHA256_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SHA256_Final_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SHA256_Init_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SHA256_Update_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SHA384_Final_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SHA384_Init_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SHA384_Update_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SHA512_Final_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SHA512_Init_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SHA512_Update_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CIPHER_get_bits_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CIPHER_get_name_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CIPHER_get_version_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_add_client_CA_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_callback_ctrl_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_check_private_key_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_ctrl_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_get_cert_store_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_get_ex_data_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_get_ex_new_index_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_get_verify_mode_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_load_verify_locations_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_load_verify_mem_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_new_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_sess_set_new_cb_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_set1_groups_list_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_set_alpn_protos_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_set_alpn_select_cb_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_set_cert_store_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_set_cert_verify_callback_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_set_cipher_list_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_set_client_cert_cb_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_set_default_passwd_cb_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_set_default_passwd_cb_userdata_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_set_ex_data_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_set_info_callback_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_set_msg_callback_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_set_next_proto_select_cb_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_set_quiet_shutdown_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_set_session_id_context_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_set_verify_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_use_PrivateKey_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_use_PrivateKey_ASN1_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_use_PrivateKey_file_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_use_certificate_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_use_certificate_ASN1_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_use_certificate_chain_file_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_use_certificate_file_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_SESSION_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_accept_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_alert_desc_string_long_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_check_private_key_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_connect_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_ctrl_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_get0_alpn_selected_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_get0_param_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_get1_session_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_get_SSL_CTX_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_get_certificate_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_get_current_cipher_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_get_error_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_get_ex_data_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_get_ex_data_X509_STORE_CTX_idx_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_get_ex_new_index_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_get_fd_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_get_peer_cert_chain_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_get_peer_certificate_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_get_privatekey_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_get_rbio_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_get_servername_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_get_session_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_get_shutdown_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_get_verify_result_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_get_wbio_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_library_init_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_load_error_strings_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_new_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_peek_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_pending_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_read_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_select_next_proto_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_set_SSL_CTX_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_set_bio_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_set_connect_state_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_set_ex_data_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_set_fd_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_set_session_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_set_verify_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_shutdown_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_use_PrivateKey_ASN1_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_use_certificate_ASN1_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_version_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_want_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_write_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSLeay_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSLv23_client_method_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSLv23_server_method_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI TLS_client_method_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI UI_OpenSSL_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI UI_create_method_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI UI_destroy_method_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI UI_get0_user_data_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI UI_get_input_flags_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI UI_get_string_type_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI UI_method_get_closer_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI UI_method_get_opener_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI UI_method_get_reader_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI UI_method_get_writer_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI UI_method_set_closer_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI UI_method_set_opener_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI UI_method_set_reader_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI UI_method_set_writer_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI UI_set_result_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509V3_EXT_print_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_EXTENSION_get_critical_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_EXTENSION_get_data_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_EXTENSION_get_object_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_INFO_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_LOOKUP_file_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_NAME_ENTRY_get_data_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_NAME_get_entry_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_NAME_get_index_by_NID_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_NAME_get_text_by_NID_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_NAME_oneline_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_NAME_print_ex_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_STORE_CTX_get1_chain_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_STORE_CTX_get_chain_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_STORE_CTX_get_current_cert_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_STORE_CTX_get_error_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_STORE_CTX_get_error_depth_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_STORE_CTX_get_ex_data_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_STORE_CTX_set_error_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_STORE_add_cert_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_STORE_add_crl_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_STORE_add_lookup_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_STORE_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_STORE_new_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_STORE_set_flags_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_VERIFY_PARAM_set1_host_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_VERIFY_PARAM_set1_ip_asc_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_VERIFY_PARAM_set_hostflags_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_check_issued_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_get_ext_d2i_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_get_issuer_name_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_get_pubkey_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_get_serialNumber_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_get_subject_name_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_load_crl_file_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_print_ex_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_verify_cert_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_verify_cert_error_string_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI d2i_OCSP_RESPONSE_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI d2i_PKCS12_bio_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI d2i_PKCS12_fp_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI d2i_RSA_PUBKEY_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI d2i_X509_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI i2a_ASN1_OBJECT_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI i2d_X509_PUBKEY_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI i2t_ASN1_OBJECT_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLibreSslGetMemoryPoolStats(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLibreSslInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLibreSslInitEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLibreSslInitEx2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLibreSslTerm(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sk_num_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sk_pop_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sk_pop_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sk_value_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceLibreSsl3/Export.cpp b/core/libs/prx/libSceLibreSsl3/Export.cpp index d81f4a5e32..d3289b659b 100644 --- a/core/libs/prx/libSceLibreSsl3/Export.cpp +++ b/core/libs/prx/libSceLibreSsl3/Export.cpp @@ -6,5 +6,439 @@ extern "C" { APS5_DUMMY_FUN +int APS5_VABI ASN1_INTEGER_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ASN1_OBJECT_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ASN1_STRING_get0_data_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BIO_up_ref_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI BIO_vfree_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI CRYPTO_memcmp_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI DH_get0_key_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI DH_get0_pqg_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI DSA_get0_key_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI DSA_get0_pqg_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EC_KEY_up_ref_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ERR_load_crypto_strings_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ERR_peek_last_error_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ERR_print_errors_cb_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_CIPHER_CTX_set_flags_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_DigestInit_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_DigestSignFinal_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_DigestSignInit_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_EncryptInit_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_MD_CTX_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_MD_CTX_new_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_PKCS82PKEY_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_PKEY_CTX_ctrl_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_PKEY_CTX_new_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_PKEY_decrypt_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_PKEY_decrypt_init_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_PKEY_encrypt_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_PKEY_encrypt_init_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_PKEY_get0_DH_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_PKEY_get0_DSA_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_PKEY_get0_RSA_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_PKEY_new_mac_key_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_PKEY_set1_EC_KEY_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_PKEY_size_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_PKEY_up_ref_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_SignFinal_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_VerifyFinal_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_aes_128_wrap_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_aes_192_wrap_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_aes_256_wrap_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_sha1_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI EVP_sha224_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI MD5_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OBJ_cmp_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OBJ_txt2obj_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OCSP_CERTID_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OCSP_cert_to_id_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OCSP_resp_find_status_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI OpenSSL_version_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI PKCS7_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI PKCS8_PRIV_KEY_INFO_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI RSA_check_key_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI RSA_get0_key_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI RSA_padding_add_PKCS1_PSS_mgf1_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI RSA_up_ref_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SHA1_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_add0_chain_cert_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_clear_chain_certs_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_set_max_proto_version_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_set_min_proto_version_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_CTX_up_ref_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_SESSION_up_ref_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_get_client_random_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI SSL_up_ref_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_PUBKEY_get0_param_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_STORE_CTX_free_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_STORE_CTX_init_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_STORE_CTX_new_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_STORE_up_ref_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_get0_extensions_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_get0_notAfter_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_get0_notBefore_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_get0_signature_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_get_ext_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_get_ext_count_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_get_version_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_getm_notAfter_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_getm_notBefore_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI X509_up_ref_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI d2i_ASN1_INTEGER_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI d2i_ECParameters_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI d2i_PKCS7_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI d2i_PKCS8_PRIV_KEY_INFO_bio_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI d2i_PrivateKey_bio_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI d2i_RSAPublicKey_bio_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI d2i_X509_bio_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI o2i_ECPublicKey_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceLoginDialog/Export.cpp b/core/libs/prx/libSceLoginDialog/Export.cpp index a986655ab2..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,17 +66,17 @@ 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) { if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } diff --git a/core/libs/prx/libSceM4aacEnc/CMakeLists.txt b/core/libs/prx/libSceM4aacEnc/CMakeLists.txt new file mode 100644 index 0000000000..85a6f282a7 --- /dev/null +++ b/core/libs/prx/libSceM4aacEnc/CMakeLists.txt @@ -0,0 +1,15 @@ +add_library(libSceM4aacEnc SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceM4aacEnc PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceM4aacEnc) + +if(BUILD_TESTING) + add_test_executable(guest_m4aac_enc_tests ${LIBS_INCLUDE_DIR}/tests/GuestM4aacEnc.cpp) + target_include_directories(guest_m4aac_enc_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_m4aac_enc_tests PRIVATE libSceM4aacEnc libc) + configure_windows_unwind(guest_m4aac_enc_tests) + add_test(NAME guest_m4aac_enc COMMAND guest_m4aac_enc_tests) + if(WIN32) + set_tests_properties(guest_m4aac_enc PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceM4aacEnc/Export.cpp b/core/libs/prx/libSceM4aacEnc/Export.cpp new file mode 100644 index 0000000000..e71f829cca --- /dev/null +++ b/core/libs/prx/libSceM4aacEnc/Export.cpp @@ -0,0 +1,94 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + + +extern "C" { + +int32_t APS5_VABI sceM4aacEncClearContext(int32_t m4aacEncHandle) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceM4aacEncClearContextEx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int32_t APS5_VABI sceM4aacEncCreateEncoder(int32_t channels, int32_t samplingRate, int32_t bitRate, int32_t inputFormat, int32_t outputFormat, int32_t *pInternalError) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceM4aacEncCreateEncoderEx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int32_t APS5_VABI sceM4aacEncDeleteEncoder(int32_t m4aacEncHandle, int32_t *pInternalError) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceM4aacEncDeleteEncoderEx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int32_t APS5_VABI sceM4aacEncEncode(int32_t m4aacEncHandle, void *pInput, uint32_t inputByte, void *pOutput, uint32_t *pOutputByte, int32_t *pInternalError) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceM4aacEncEncodeEx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceM4aacEncEncodeWithPriority() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceM4aacEncEncodeWithPriorityEx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int32_t APS5_VABI sceM4aacEncFlush(int32_t m4aacEncHandle, void *pOutput, uint32_t *pOutputByte, int32_t *pInternalError) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceM4aacEncFlushEx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceM4aacEncFlushWithPriority() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceM4aacEncFlushWithPriorityEx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceM4aacEncGetContext() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceM4aacEncInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceM4aacEncTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} 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..907eeb12c0 --- /dev/null +++ b/core/libs/prx/libSceMbus/PendingExports.cpp @@ -0,0 +1,578 @@ +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceMbusAcquireControl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMbusAcquireControlList() { + return static_cast(0x809B00FF); +} + +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() { + 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/libSceMouse/tests/Mouse.cpp b/core/libs/prx/libSceMouse/tests/Mouse.cpp index ffada31501..3fd5a90ca2 100644 --- a/core/libs/prx/libSceMouse/tests/Mouse.cpp +++ b/core/libs/prx/libSceMouse/tests/Mouse.cpp @@ -7,6 +7,7 @@ #include extern "C" { +bool MouseIsOpen_nid_no_patch(); int APS5_VABI sceMouseInit(); int APS5_VABI sceMouseOpen(int, std::int32_t, std::int32_t, const void*); int APS5_VABI sceMouseClose(std::int32_t); @@ -18,6 +19,7 @@ static void Require(bool value) { if (!value) std::abort(); } int main() { MouseData data[64]{}; + Require(!MouseIsOpen_nid_no_patch()); Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 959, 539) == MOUSE_ERROR_NOT_INITIALIZED); Require(sceMouseOpen(1, 0, 0, nullptr) == MOUSE_ERROR_NOT_INITIALIZED); Require(sceMouseInit() == MOUSE_OK); @@ -28,6 +30,7 @@ int main() { unsupported.behaviorFlag = 2; Require(sceMouseOpen(1, 0, 0, &unsupported) == MOUSE_ERROR_INVALID_ARG); Require(sceMouseOpen(1, 0, 0, nullptr) == MOUSE_HANDLE); + Require(MouseIsOpen_nid_no_patch()); Require(sceMouseOpen(1, 0, 0, nullptr) == MOUSE_ERROR_ALREADY_OPENED); Require(sceMouseResetAbsolutePosition(42, 959, 539) == MOUSE_ERROR_INVALID_HANDLE); Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 959, 539) == MOUSE_OK); @@ -103,14 +106,17 @@ int main() { Require(data[0].x_axis == 6 && data[63].x_axis == 69); Require(sceMouseRead(MOUSE_HANDLE, data, 1) == 0); Require(sceMouseClose(MOUSE_HANDLE) == MOUSE_OK); + Require(!MouseIsOpen_nid_no_patch()); Require(sceMouseRead(MOUSE_HANDLE, data, 1) == MOUSE_ERROR_INVALID_HANDLE); Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 959, 539) == MOUSE_ERROR_INVALID_HANDLE); Require(sceMouseClose(MOUSE_HANDLE) == MOUSE_ERROR_INVALID_HANDLE); MouseOpenParam merged{}; merged.behaviorFlag = MOUSE_OPEN_PARAM_MERGED; Require(sceMouseOpen(1, 0, 0, &merged) == MOUSE_HANDLE); + Require(MouseIsOpen_nid_no_patch()); Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 959, 539) == MOUSE_OK); Require(sceMouseRead(MOUSE_HANDLE, data, 1) == 1 && data[0].buttons == 0); Require(sceMouseClose(MOUSE_HANDLE) == MOUSE_OK); + Require(!MouseIsOpen_nid_no_patch()); Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 959, 539) == MOUSE_ERROR_INVALID_HANDLE); } diff --git a/core/libs/prx/libSceMove/Export.cpp b/core/libs/prx/libSceMove/Export.cpp index 6c2199ff94..2ffc51523e 100644 --- a/core/libs/prx/libSceMove/Export.cpp +++ b/core/libs/prx/libSceMove/Export.cpp @@ -1,31 +1,164 @@ #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 int MOVE_ERROR_INVALID_HANDLE = static_cast(0x80EE0004); +constexpr int MOVE_ERROR_ALREADY_OPENED = static_cast(0x80EE0007); +constexpr int32_t MOVE_FIRST_HANDLE = 0x30B0100; +constexpr int32_t MOVE_HANDLE_STEP = 0x100; + +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(int32_t handle) { + std::lock_guard lock(stateMutex); + if (!initialized) return MOVE_ERROR_NOT_INIT; + return openHandles.contains(handle) ? MOVE_OK : MOVE_ERROR_INVALID_HANDLE; +} + +} 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) return MOVE_ERROR_ALREADY_OPENED; + } + 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; + return openHandles.erase(handle) != 0 ? MOVE_OK : MOVE_ERROR_INVALID_HANDLE; } + +int APS5_VABI sceMoveGetDeviceInfo(int32_t handle, void* info) { + if (const int result = checkHandle(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(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(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(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(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) { + (void)red; + (void)green; + (void)blue; + if (const int result = checkHandle(handle); result != MOVE_OK) return result; + return MOVE_NO_CONTROLLER_CONNECTED; +} + +int APS5_VABI sceMoveResetLightSphere(int32_t handle) { + return checkHandle(handle); +} +int APS5_VABI sceMoveGetBluetoothAddress(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveGetCalibrationInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveGetDeviceId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveIsReproductionModel(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveIsValidHandle(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveReadStateRecentForTracker(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveSetCalibrationMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveSetExtensionPortOutput(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveSetLightSphereForTracker(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMoveWriteCalibrationInfo(void) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceMoveTracker/Export.cpp b/core/libs/prx/libSceMoveTracker/Export.cpp index d81f4a5e32..8b2c497e09 100644 --- a/core/libs/prx/libSceMoveTracker/Export.cpp +++ b/core/libs/prx/libSceMoveTracker/Export.cpp @@ -1,10 +1,96 @@ -#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() { + return 2; +} + +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() { + 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/CMakeLists.txt b/core/libs/prx/libSceNet/CMakeLists.txt index cdab277419..95faf4584b 100644 --- a/core/libs/prx/libSceNet/CMakeLists.txt +++ b/core/libs/prx/libSceNet/CMakeLists.txt @@ -1,8 +1,36 @@ add_library(libSceNet SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceNet PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceNet PRIVATE libSceNetCtl) configure_windows_unwind(libSceNet) if(BUILD_TESTING) + add_test_executable(net_dontwait_tests ${LIBS_INCLUDE_DIR}/tests/NetDontWait.cpp) + target_include_directories(net_dontwait_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(net_dontwait_tests PRIVATE libSceNet libkernel libc) + configure_windows_unwind(net_dontwait_tests) + foreach(api IN ITEMS send sendto sendmsg recv recvfrom recvmsg duplex blocking) + add_test(NAME net_dontwait_${api} COMMAND net_dontwait_tests ${api}) + set_tests_properties(net_dontwait_${api} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(net_dontwait_${api} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() + add_test_executable(net_blocking_send_tests ${LIBS_INCLUDE_DIR}/tests/NetBlockingSend.cpp) + target_include_directories(net_blocking_send_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(net_blocking_send_tests PRIVATE libkernel libc libSceNetCtl) + configure_windows_unwind(net_blocking_send_tests) + if(WIN32) + target_link_libraries(net_blocking_send_tests PRIVATE ws2_32) + endif() + foreach(api IN ITEMS send sendto sendmsg) + add_test(NAME net_dontwait_blocking_${api} COMMAND net_blocking_send_tests ${api}) + set_tests_properties(net_dontwait_blocking_${api} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(net_dontwait_blocking_${api} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() add_test_executable(guest_datagram_receive_tests ${LIBS_INCLUDE_DIR}/tests/GuestDatagramReceive.cpp) target_include_directories(guest_datagram_receive_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_datagram_receive_tests PRIVATE libSceNet libkernel libc) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index f77cc1b95d..82fb9cbd4c 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -28,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -36,8 +38,11 @@ #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/SocketError.hpp" #include "prx/libkernel/Socket/include/SocketPoll.hpp" #include "prx/libkernel/Socket/include/SocketRuntime.hpp" +#include "prx/libkernel/Time/include/Time.hpp" // Guest socket descriptors retain PS5 semantics while their transport is backed by host sockets. @@ -55,6 +60,7 @@ constexpr int NET_EAGAIN = 35; constexpr int NET_EINPROGRESS = 36; constexpr int NET_EALREADY = 37; constexpr int NET_ENOTSOCK = 38; +constexpr int NET_ENOPROTOOPT = 42; constexpr int NET_EOPNOTSUPP = 45; constexpr int NET_EPROTONOSUPPORT = 43; constexpr int NET_EAFNOSUPPORT = 47; @@ -83,6 +89,9 @@ constexpr int NET_EPOLL_CTL_ADD = 1; constexpr int NET_EPOLL_CTL_MOD = 2; constexpr int NET_EPOLL_CTL_DEL = 3; constexpr int NET_SOL_SOCKET = 0xFFFF; +constexpr int NET_IPPROTO_TCP = 6; +constexpr int NET_TCP_NODELAY = 1; +constexpr int NET_TF_NODELAY = 4; constexpr int NET_SO_SNDTIMEO = 0x1005; constexpr int NET_SO_RCVTIMEO = 0x1006; constexpr int NET_SO_ERROR = 0x1007; @@ -112,32 +121,7 @@ bool initialize_sockets() { return result == 0; } void close_socket(NativeSocket socket) { closesocket(socket); } -int net_error(int code) { - switch (code) { - case WSAEWOULDBLOCK: return NET_EAGAIN; - case WSAEINPROGRESS: return NET_EINPROGRESS; - case WSAEALREADY: return NET_EALREADY; - case WSAEADDRINUSE: return NET_EADDRINUSE; - case WSAEADDRNOTAVAIL: return NET_EADDRNOTAVAIL; - case WSAEAFNOSUPPORT: return NET_EAFNOSUPPORT; - case WSAENETUNREACH: return NET_ENETUNREACH; - case WSAEHOSTUNREACH: return NET_EHOSTUNREACH; - case WSAECONNABORTED: return NET_ECONNABORTED; - case WSAENOBUFS: return NET_ENOBUFS; - case WSAEISCONN: return NET_EISCONN; - case WSAENOTCONN: return NET_ENOTCONN; - case WSAEACCES: return NET_EACCES; - case WSAECONNRESET: return 54; - case WSAETIMEDOUT: return NET_ETIMEDOUT; - case WSAECONNREFUSED: return NET_ECONNREFUSED; - case WSAEMSGSIZE: return NET_EMSGSIZE; - case WSAEINTR: return 4; - case WSAEINVAL: return NET_EINVAL; - case WSAENOTSOCK: return NET_ENOTSOCK; - case WSAESHUTDOWN: return 32; - default: return 5; - } -} +int net_error(int code) { return GuestSockets::GuestErrorFromHost(code); } int native_error() { return net_error(WSAGetLastError()); } #else using NativeSocket = int; @@ -146,41 +130,25 @@ constexpr NativeSocket INVALID_NATIVE_SOCKET = -1; constexpr int NATIVE_SEND_FLAGS = MSG_NOSIGNAL; bool initialize_sockets() { return true; } void close_socket(NativeSocket socket) { ::close(socket); } -int net_error(int code) { - switch (code) { - case EAGAIN: return NET_EAGAIN; -#if EWOULDBLOCK != EAGAIN - case EWOULDBLOCK: return NET_EAGAIN; -#endif - case EINPROGRESS: return NET_EINPROGRESS; - case EALREADY: return NET_EALREADY; - case EADDRINUSE: return NET_EADDRINUSE; - case EADDRNOTAVAIL: return NET_EADDRNOTAVAIL; - case EAFNOSUPPORT: return NET_EAFNOSUPPORT; - case ENETUNREACH: return NET_ENETUNREACH; - case EHOSTUNREACH: return NET_EHOSTUNREACH; - case ECONNABORTED: return NET_ECONNABORTED; - case ENOBUFS: return NET_ENOBUFS; - case EISCONN: return NET_EISCONN; - case ENOTCONN: return NET_ENOTCONN; - case EACCES: return NET_EACCES; - case ECONNRESET: return 54; - case ETIMEDOUT: return NET_ETIMEDOUT; - case ECONNREFUSED: return NET_ECONNREFUSED; - case EMSGSIZE: return NET_EMSGSIZE; - case EINTR: return 4; - case EINVAL: return NET_EINVAL; - case ENOTSOCK: return NET_ENOTSOCK; - case EPIPE: return 32; - default: return 5; - } -} +int net_error(int code) { return GuestSockets::GuestErrorFromHost(code); } int native_error() { return net_error(errno); } #endif -struct NativeSocketHandle { +#ifdef _WIN32 +int TransferSocketForKernel(KernelSocketPoll::NativeOwner* owner, bool send, void* buffer, int length); +#endif + +struct NativeSocketHandle : KernelSocketPoll::NativeOwner { NativeSocket value; - explicit NativeSocketHandle(NativeSocket socket) : value(socket) {} + const bool stream; + std::atomic nonblock{false}; + std::atomic receiveTimeout{0}; + std::atomic sendTimeout{0}; + NativeSocketHandle(NativeSocket socket, bool stream) : value(socket), stream(stream) { +#ifdef _WIN32 + transfer = &TransferSocketForKernel; +#endif + } ~NativeSocketHandle() { if (value != INVALID_NATIVE_SOCKET) close_socket(value); } @@ -245,13 +213,10 @@ struct Sock { int family = 0; int type = 0; std::shared_ptr native; - bool nonblock = false; bool bound = false; bool listening = false; bool aborted = false; bool read_shut = false; - int rcv_timeout_us = 0; - int snd_timeout_us = 0; int reported_error = 0; int snd_buffer = 0; int rcv_buffer = 0; @@ -341,6 +306,7 @@ int* errno_slot() { } extern "C" int* APS5_VABI __error_nid_postfix(); +extern "C" std::size_t NetCtlHostDnsServers_nid_no_patch(std::uint32_t* servers, std::size_t capacity); void set_errno(int err) { *errno_slot() = err; @@ -385,8 +351,8 @@ int set_kernel_timeout(int descriptor, int optname, const void* value, std::uint const auto it = g_socks.find(descriptor); if (it != g_socks.end()) { const int stored = static_cast(std::min(microseconds, INT_MAX)); - if (optname == NET_SO_RCVTIMEO) it->second.rcv_timeout_us = stored; - else it->second.snd_timeout_us = stored; + if (optname == NET_SO_RCVTIMEO) it->second.native->receiveTimeout = stored; + else it->second.native->sendTimeout = stored; } return 0; } @@ -474,7 +440,7 @@ std::int64_t message_length(const NetMsghdr* message) { int receive_error(const Sock& socket) { #ifdef _WIN32 - if (WSAGetLastError() == WSAETIMEDOUT && socket.type != NET_SOCK_STREAM && socket.rcv_timeout_us > 0) return NET_EAGAIN; + if (WSAGetLastError() == WSAETIMEDOUT && socket.type != NET_SOCK_STREAM && socket.native->receiveTimeout > 0) return NET_EAGAIN; #else (void)socket; #endif @@ -482,12 +448,63 @@ 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; } +template +auto native_transfer(const NativeSocketHandle& socket, int flags, bool send, TOperation operation, bool timed = true, int length = 0) { +#ifdef _WIN32 + using Result = decltype(operation(0)); + const bool nonblocking = (flags & NET_MSG_DONTWAIT) != 0 || socket.nonblock.load(); + const int completeLength = send && socket.stream && !nonblocking ? length : 0; + const int timeout = timed ? (send ? socket.sendTimeout.load() : socket.receiveTimeout.load()) : 0; + const auto deadline = timeout > 0 ? std::chrono::steady_clock::now() + std::chrono::microseconds(timeout) + : std::chrono::steady_clock::time_point::max(); + int transferred = 0; + for (;;) { + const auto result = operation(transferred); + if (result != static_cast(-1)) { + if (completeLength == 0) return result; + transferred += static_cast(result); + if (transferred == completeLength || result == 0) return static_cast(transferred); + } else if (WSAGetLastError() != WSAEWOULDBLOCK || nonblocking) { + return transferred > 0 ? static_cast(transferred) : result; + } + int milliseconds = 10; + if (timeout > 0) { + const auto remaining = deadline - std::chrono::steady_clock::now(); + if (remaining <= std::chrono::steady_clock::duration::zero()) { + WSASetLastError(completeLength > 0 ? WSAEWOULDBLOCK : WSAETIMEDOUT); + return transferred > 0 ? static_cast(transferred) : static_cast(-1); + } + milliseconds = static_cast(std::min(std::chrono::ceil(remaining).count(), milliseconds)); + } + WSAPOLLFD descriptor{}; + descriptor.fd = socket.value; + descriptor.events = send ? POLLWRNORM : POLLRDNORM; + const int ready = WSAPoll(&descriptor, 1, milliseconds); + if (ready < 0) return transferred > 0 ? static_cast(transferred) : static_cast(-1); + } +#else + static_cast(socket); + static_cast(flags); + static_cast(send); + static_cast(timed); + static_cast(length); + return operation(0); +#endif +} + +#ifdef _WIN32 +int TransferSocketForKernel(KernelSocketPoll::NativeOwner* owner, bool send, void* buffer, int length) { + const auto* socket = static_cast(owner); + return native_transfer(*socket, 0, send, [&](int offset) { + return send ? ::send(socket->value, offset ? static_cast(buffer) + offset : static_cast(buffer), length - offset, 0) + : ::recv(socket->value, static_cast(buffer), length, 0); + }, true, length); +} +#endif + int native_message_flags(int flags) { int native = (flags & NET_MSG_PEEK) ? MSG_PEEK : 0; #ifndef _WIN32 @@ -524,13 +541,14 @@ 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; - const auto deadline = socket.rcv_timeout_us > 0 - ? std::chrono::steady_clock::now() + std::chrono::microseconds(socket.rcv_timeout_us) + if (!named && (socket.native->nonblock || (flags & NET_MSG_DONTWAIT) != 0)) return NET_EAGAIN; + const int timeout = socket.native->receiveTimeout; + const auto deadline = timeout > 0 + ? std::chrono::steady_clock::now() + std::chrono::microseconds(timeout) : std::chrono::steady_clock::time_point::max(); std::unique_lock lk(g_mutex); for (bool waited = false;; waited = true) { @@ -549,6 +567,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" { @@ -666,7 +724,7 @@ bool ControlSocketStatusForKernel(int descriptor, int command, int flags, int* r std::lock_guard lk(g_mutex); const auto it = g_socks.find(descriptor); if (it == g_socks.end()) return false; - nonblock = it->second.nonblock; + nonblock = it->second.native->nonblock; } *result = 0; *error = 0; @@ -694,7 +752,7 @@ bool FindSocketNativeForKernel(int descriptor, std::shared_ptr* owner, std const auto it = g_socks.find(descriptor); if (it == g_socks.end() || !it->second.native) return false; *native = static_cast(it->second.native->value); - *owner = it->second.native; + *owner = std::static_pointer_cast(it->second.native); return true; } @@ -740,6 +798,14 @@ int APS5_VABI sceNetSocket(const char* name, int family, int type, int protocol) if (!initialize_sockets()) return fail(5); const NativeSocket native = ::socket(family == NET_AF_INET ? AF_INET : AF_INET6, type, protocol); if (native == INVALID_NATIVE_SOCKET) return fail(native_error()); +#ifdef _WIN32 + u_long nonblocking = 1; + if (ioctlsocket(native, FIONBIO, &nonblocking) != 0) { + const int error = native_error(); + close_socket(native); + return fail(error); + } +#endif const int ipv6_only = 1; if (family == NET_AF_INET6 && ::setsockopt(native, IPPROTO_IPV6, IPV6_V6ONLY, reinterpret_cast(&ipv6_only), sizeof(ipv6_only)) != 0) { @@ -749,7 +815,7 @@ int APS5_VABI sceNetSocket(const char* name, int family, int type, int protocol) } std::shared_ptr handle; try { - handle = std::make_shared(native); + handle = std::make_shared(native, type == NET_SOCK_STREAM); } catch (const std::bad_alloc&) { close_socket(native); return fail(12); @@ -867,12 +933,15 @@ int APS5_VABI sceNetAccept(int s, void* addr, uint32_t* addrlen) { if (addr && !addrlen) return fail(NET_EINVAL); sockaddr_storage peer{}; NativeLength peer_length = sizeof(peer); - const NativeSocket accepted = ::accept(listener.native->value, - addr ? reinterpret_cast(&peer) : nullptr, addr ? &peer_length : nullptr); + const NativeSocket accepted = native_transfer(*listener.native, 0, false, [&](int) { + peer_length = sizeof(peer); + return ::accept(listener.native->value, + addr ? reinterpret_cast(&peer) : nullptr, addr ? &peer_length : nullptr); + }, false); if (accepted == INVALID_NATIVE_SOCKET) return fail(native_error()); std::shared_ptr handle; try { - handle = std::make_shared(accepted); + handle = std::make_shared(accepted, true); } catch (const std::bad_alloc&) { close_socket(accepted); return fail(12); @@ -881,13 +950,17 @@ int APS5_VABI sceNetAccept(int s, void* addr, uint32_t* addrlen) { connection.family = listener.family; connection.type = NET_SOCK_STREAM; connection.native = std::move(handle); +#ifdef _WIN32 + connection.native->receiveTimeout = listener.native->receiveTimeout.load(); + connection.native->sendTimeout = listener.native->sendTimeout.load(); +#endif const int descriptor = KernelReserveSocketNumber_nid_no_patch(static_cast(connection.native->value)); if (descriptor < 0) return fail(-descriptor); { std::lock_guard lk(g_mutex); g_socks.emplace(descriptor, connection); } - if (listener.nonblock) { + if (listener.native->nonblock) { const int enabled = 1; if (sceNetSetsockopt(descriptor, NET_SOL_SOCKET, NET_SO_NBIO, &enabled, sizeof(enabled)) != 0) { const int error = *errno_slot(); @@ -965,7 +1038,21 @@ int APS5_VABI sceNetConnect(int s, const void* addr, uint32_t addrlen) { return 0; } #ifdef _WIN32 - if (WSAGetLastError() == WSAEWOULDBLOCK) return fail(NET_EINPROGRESS); + if (WSAGetLastError() == WSAEWOULDBLOCK) { + if (socket.native->nonblock) return fail(NET_EINPROGRESS); + fd_set writable; + fd_set failed; + FD_ZERO(&writable); + FD_ZERO(&failed); + FD_SET(socket.native->value, &writable); + FD_SET(socket.native->value, &failed); + if (::select(0, nullptr, &writable, &failed, nullptr) < 0) return fail(native_error()); + int error = 0; + int size = sizeof(error); + if (::getsockopt(socket.native->value, SOL_SOCKET, SO_ERROR, reinterpret_cast(&error), &size) != 0) + return fail(native_error()); + return error == 0 ? 0 : fail(net_error(error)); + } #endif return fail(native_error()); } @@ -982,12 +1069,14 @@ 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); const int native_flags = native_message_flags(flags); - const int result = ::recv(socket.native->value, static_cast(buf), static_cast(len), native_flags); + const int result = native_transfer(*socket.native, flags, false, [&](int) { + return ::recv(socket.native->value, static_cast(buf), static_cast(len), native_flags); + }); #ifdef _WIN32 if (result < 0 && socket.type == NET_SOCK_DGRAM && WSAGetLastError() == WSAEMSGSIZE) return static_cast(len); #endif @@ -1009,7 +1098,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; @@ -1024,8 +1113,11 @@ int64_t APS5_VABI sceNetRecvfrom(int s, void* buf, size_t len, int flags, void* sockaddr_storage peer{}; NativeLength peer_length = sizeof(peer); 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); + int result = native_transfer(*socket.native, flags, false, [&](int) { + peer_length = sizeof(peer); + return ::recvfrom(socket.native->value, static_cast(buf), static_cast(len), + native_flags, from ? reinterpret_cast(&peer) : nullptr, from ? &peer_length : nullptr); + }); #ifdef _WIN32 if (result < 0 && socket.type == NET_SOCK_DGRAM && WSAGetLastError() == WSAEMSGSIZE) result = static_cast(len); #endif @@ -1045,7 +1137,9 @@ 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; } - const int result = ::send(socket.native->value, static_cast(buf), static_cast(len), NATIVE_SEND_FLAGS | native_message_flags(flags)); + const int result = native_transfer(*socket.native, flags, true, [&](int offset) { + return ::send(socket.native->value, offset ? static_cast(buf) + offset : static_cast(buf), static_cast(len) - offset, NATIVE_SEND_FLAGS | native_message_flags(flags)); + }, true, static_cast(len)); return result >= 0 ? result : fail(native_error()); } @@ -1063,14 +1157,18 @@ int64_t APS5_VABI sceNetSendto(int s, const void* buf, size_t len, int flags, co int result; if (!to) { if (tolen != 0) return fail(NET_EINVAL); - result = ::send(socket.native->value, static_cast(buf), static_cast(len), NATIVE_SEND_FLAGS | native_message_flags(flags)); + result = native_transfer(*socket.native, flags, true, [&](int offset) { + return ::send(socket.native->value, offset ? static_cast(buf) + offset : static_cast(buf), static_cast(len) - offset, NATIVE_SEND_FLAGS | native_message_flags(flags)); + }, true, static_cast(len)); } else { sockaddr_storage destination{}; NativeLength destination_length = 0; if (!guest_to_native_address(to, tolen, destination, destination_length)) return fail(NET_EINVAL); if (destination.ss_family != (socket.family == NET_AF_INET ? AF_INET : AF_INET6)) return fail(NET_EAFNOSUPPORT); - result = ::sendto(socket.native->value, static_cast(buf), static_cast(len), NATIVE_SEND_FLAGS | native_message_flags(flags), - reinterpret_cast(&destination), destination_length); + result = native_transfer(*socket.native, flags, true, [&](int offset) { + return ::sendto(socket.native->value, offset ? static_cast(buf) + offset : static_cast(buf), static_cast(len) - offset, NATIVE_SEND_FLAGS | native_message_flags(flags), + reinterpret_cast(&destination), destination_length); + }, true, static_cast(len)); } if (result < 0) return fail(native_error()); #ifdef _WIN32 @@ -1118,7 +1216,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; @@ -1134,8 +1232,11 @@ int64_t APS5_VABI sceNetRecvmsg(int s, NetMsghdr* msg, int flags) { } sockaddr_storage peer{}; NativeLength peer_length = sizeof(peer); - std::int64_t received = ::recvfrom(socket.native->value, buffer.data(), static_cast(buffer.size()), - native_message_flags(flags), reinterpret_cast(&peer), &peer_length); + std::int64_t received = native_transfer(*socket.native, flags, false, [&](int) { + peer_length = sizeof(peer); + return ::recvfrom(socket.native->value, buffer.data(), static_cast(buffer.size()), + native_message_flags(flags), reinterpret_cast(&peer), &peer_length); + }); if (received < 0) { #ifdef _WIN32 if (!datagram || WSAGetLastError() != WSAEMSGSIZE) return fail(receive_error(socket)); @@ -1196,6 +1297,17 @@ int APS5_VABI sceNetSetsockopt(int s, int level, int optname, const void* optval if (!GuestSocketIsOpen_nid_no_patch(s)) return fail(NET_EBADF); return setsockopt_nid_postfix(s, level, optname, optval, optlen) == 0 ? 0 : fail(*__error_nid_postfix()); } + if (level == NET_IPPROTO_TCP && socket.type != NET_SOCK_STREAM) return fail(NET_EINVAL); + if (level == NET_IPPROTO_TCP && optname == NET_TCP_NODELAY) { + if (!optval || optlen < sizeof(int)) return fail(NET_EINVAL); + int value = 0; + std::memcpy(&value, optval, sizeof(value)); + const int enabled = value != 0; + if (::setsockopt(socket.native->value, IPPROTO_TCP, TCP_NODELAY, reinterpret_cast(&enabled), sizeof(enabled)) != 0) + return fail(native_error()); + return 0; + } + if (level == NET_IPPROTO_TCP) return fail(NET_ENOPROTOOPT); if (level != NET_SOL_SOCKET) return fail(NET_EOPNOTSUPP); if (optname == NET_SO_RCVTIMEO || optname == NET_SO_SNDTIMEO) { const int error = set_kernel_timeout(s, optname, optval, optlen); @@ -1206,10 +1318,7 @@ int APS5_VABI sceNetSetsockopt(int s, int level, int optname, const void* optval int value = 0; std::memcpy(&value, optval, sizeof(value)); if (optname == NET_SO_NBIO) { -#ifdef _WIN32 - u_long enabled = value != 0; - if (ioctlsocket(socket.native->value, FIONBIO, &enabled) != 0) return fail(native_error()); -#else +#ifndef _WIN32 const int old_flags = fcntl(socket.native->value, F_GETFL, 0); if (old_flags < 0 || fcntl(socket.native->value, F_SETFL, value ? old_flags | O_NONBLOCK : old_flags & ~O_NONBLOCK) != 0) return fail(native_error()); @@ -1228,9 +1337,9 @@ int APS5_VABI sceNetSetsockopt(int s, int level, int optname, const void* optval std::lock_guard lk(g_mutex); auto it = g_socks.find(s); if (it != g_socks.end()) { - if (optname == NET_SO_NBIO) it->second.nonblock = value != 0; - if (optname == NET_SO_RCVTIMEO) it->second.rcv_timeout_us = value; - if (optname == NET_SO_SNDTIMEO) it->second.snd_timeout_us = value; + if (optname == NET_SO_NBIO) it->second.native->nonblock = value != 0; + if (optname == NET_SO_RCVTIMEO) it->second.native->receiveTimeout = value; + if (optname == NET_SO_SNDTIMEO) it->second.native->sendTimeout = value; if (optname == NET_SO_SNDBUF) it->second.snd_buffer = value; if (optname == NET_SO_RCVBUF) it->second.rcv_buffer = value; if (optname == NET_SO_ONESBCAST) it->second.onesbcast = value; @@ -1248,19 +1357,31 @@ int APS5_VABI sceNetGetsockopt(int s, int level, int optname, void* optval, uint } if (optval == nullptr || optlen == nullptr) return fail(NET_EINVAL); if (level == NET_SOL_SOCKET && (optname == NET_SO_RCVTIMEO || optname == NET_SO_SNDTIMEO)) { - const int microseconds = optname == NET_SO_RCVTIMEO ? socket.rcv_timeout_us : socket.snd_timeout_us; + const int microseconds = optname == NET_SO_RCVTIMEO ? socket.native->receiveTimeout.load() : socket.native->sendTimeout.load(); const KernelTimeval timeout{microseconds / 1000000, microseconds % 1000000}; *optlen = std::min(*optlen, static_cast(sizeof(timeout))); std::memcpy(optval, &timeout, *optlen); return 0; } if (*optlen < sizeof(int)) return fail(NET_EINVAL); + if (level == NET_IPPROTO_TCP && socket.type != NET_SOCK_STREAM) return fail(NET_EINVAL); + if (level == NET_IPPROTO_TCP && optname == NET_TCP_NODELAY) { + int enabled = 0; + NativeLength size = sizeof(enabled); + if (::getsockopt(socket.native->value, IPPROTO_TCP, TCP_NODELAY, reinterpret_cast(&enabled), &size) != 0) + return fail(native_error()); + const int value = enabled != 0 ? NET_TF_NODELAY : 0; + std::memcpy(optval, &value, sizeof(value)); + *optlen = sizeof(value); + return 0; + } + if (level == NET_IPPROTO_TCP) return fail(NET_ENOPROTOOPT); if (level != NET_SOL_SOCKET) return fail(NET_EOPNOTSUPP); int value = 0; switch (timeout_option(optname)) { - case NET_SO_NBIO: value = socket.nonblock ? 1 : 0; break; - case NET_SO_RCVTIMEO: value = socket.rcv_timeout_us; break; - case NET_SO_SNDTIMEO: value = socket.snd_timeout_us; break; + case NET_SO_NBIO: value = socket.native->nonblock ? 1 : 0; break; + case NET_SO_RCVTIMEO: value = socket.native->receiveTimeout; break; + case NET_SO_SNDTIMEO: value = socket.native->sendTimeout; break; case NET_SO_ONESBCAST: value = socket.onesbcast; break; case NET_SO_ERROR: { int pending = 0; @@ -1333,12 +1454,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.native->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) { @@ -1362,7 +1587,7 @@ int APS5_VABI sceNetShowNetstatWithMemory(int memid) { poolSize = pool->second; sockets.reserve(g_socks.size()); for (const auto& [id, socket] : g_socks) - sockets.push_back({id, socket.family, socket.type, socket.nonblock, socket.bound, socket.listening}); + sockets.push_back({id, socket.family, socket.type, socket.native->nonblock, socket.bound, socket.listening}); } std::fprintf(stdout, "sceNet netstat: pool=%d bytes=%zu sockets=%zu\n", memid, poolSize, sockets.size()); for (const auto& socket : sockets) @@ -1534,11 +1759,17 @@ int APS5_VABI sceNetInetPton(int af, const char* src, void* dst) { if (af != NET_AF_INET && af != NET_AF_INET6) { return fail(NET_EAFNOSUPPORT); } + const std::string text = af == NET_AF_INET ? std::string(src, std::strcspn(src, " \t\n\v\f\r")) : std::string(src); + std::array address{}; #ifdef _WIN32 - return InetPtonA(af == NET_AF_INET ? AF_INET : AF_INET6, src, dst); + const int converted = InetPtonA(af == NET_AF_INET ? AF_INET : AF_INET6, text.c_str(), address.data()); #else - return ::inet_pton(af == NET_AF_INET ? AF_INET : AF_INET6, src, dst); + const int converted = ::inet_pton(af == NET_AF_INET ? AF_INET : AF_INET6, text.c_str(), address.data()); #endif + if (converted == 1) { + std::memcpy(dst, address.data(), af == NET_AF_INET ? 4 : 16); + } + return converted; } const char* APS5_VABI sceNetInetNtop(int af, const void* src, char* dst, uint32_t size) { @@ -1710,23 +1941,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; @@ -1734,6 +1957,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); @@ -1766,8 +2011,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; } @@ -1802,7 +2047,6 @@ int APS5_VABI sceNetShowPolicyWithMemory(void) { } int APS5_VABI sceNetShowRoute(void) { - NotImplemented_nid_no_patch(__func__); return 0; } @@ -1811,11 +2055,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; @@ -1911,9 +2150,13 @@ int APS5_VABI sceNetGetAppMemInfo(void) { return 0; } -int APS5_VABI sceNetGetDnsInfo(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNetGetDnsInfo(std::uint32_t* info, int flags) { + if (flags != 0) throw std::runtime_error("sceNetGetDnsInfo: flags " + std::to_string(flags) + " are not supported"); + if (!info) throw std::runtime_error("sceNetGetDnsInfo: a null info is not supported"); + std::array servers{}; + const std::size_t count = std::min(NetCtlHostDnsServers_nid_no_patch(servers.data(), servers.size()), servers.size()); + std::memcpy(info, servers.data(), sizeof(servers)); + return static_cast(count); } int APS5_VABI sceNetGetIfList(void) { @@ -1931,9 +2174,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) { @@ -1946,8 +2194,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; } @@ -1961,9 +2213,8 @@ int APS5_VABI sceNetGetsockoptEx(void) { return 0; } -int APS5_VABI sceNetGetSystemTime(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +void APS5_VABI sceNetGetSystemTime(std::uint64_t* output) { + if (output != nullptr) *output = sceKernelGetProcessTime(); } int APS5_VABI sceNetGetSystemMemInfo(void) { @@ -1976,9 +2227,21 @@ int APS5_VABI sceNetInitParam(void) { return 0; } -int APS5_VABI sceNetInetPtonEx(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +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) { @@ -1986,14 +2249,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) { @@ -2056,9 +2330,9 @@ int APS5_VABI sceNetThreadJoin(void) { return 0; } -int APS5_VABI sceNetUsleep(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNetUsleep(int microseconds) { + if (microseconds < 0) return fail(NET_EINVAL); + return sceKernelUsleep_nid_postfix(static_cast(microseconds)); } int APS5_VABI sceNetShowIfconfigForBuffer(void) { @@ -2616,4 +2890,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 f29a351575..c8e3080177 100644 --- a/core/libs/prx/libSceNetCtl/Export.cpp +++ b/core/libs/prx/libSceNetCtl/Export.cpp @@ -64,6 +64,8 @@ static constexpr int NET_CTL_INFO_SECONDARY_DNS = 18; static constexpr int NET_CTL_INFO_HTTP_PROXY_CONFIG = 19; static constexpr int NET_CTL_INFO_HTTP_PROXY_SERVER = 20; static constexpr int NET_CTL_INFO_HTTP_PROXY_PORT = 21; +static constexpr int NET_CTL_INFO_V6_IP_ADDRESS = 1; +static constexpr int NET_CTL_INFO_V6_SECONDARY_DNS = 5; static constexpr std::uint32_t NET_CTL_DEVICE_WIRED = 0; static constexpr std::uint32_t NET_CTL_LINK_CONNECTED = 1; static constexpr std::uint32_t NET_CTL_IP_DHCP = 0; @@ -350,6 +352,13 @@ static bool FindHostIpv4Interface(HostIpv4Interface& found) { #endif } +extern "C" std::size_t NetCtlHostDnsServers_nid_no_patch(std::uint32_t* servers, std::size_t capacity) { + HostIpv4Interface host; + if (!FindHostIpv4Interface(host)) return 0; + for (std::size_t index = 0; index < host.dns.size() && index < capacity; ++index) servers[index] = host.dns[index].s_addr; + return host.dns.size(); +} + static void WriteInfoWord(NetCtlInfo* info, std::uint32_t value) { std::memcpy(info->opaque, &value, sizeof(value)); } @@ -492,6 +501,13 @@ int APS5_VABI sceNetCtlRegisterCallbackV6(NetCtlCallback func, void* arg, int* c return RegisterCallback(g_callbacksV6, func, arg, cid); } +int APS5_VABI sceNetCtlGetInfoV6(int code, void* info) { + if (!info) return SCE_NET_CTL_ERROR_INVALID_ADDR; + if (code < NET_CTL_INFO_V6_IP_ADDRESS || code > NET_CTL_INFO_V6_SECONDARY_DNS) return SCE_NET_CTL_ERROR_INVALID_CODE; + if (!HostHasAddress(AF_INET6)) return SCE_NET_CTL_ERROR_NOT_CONNECTED; + throw std::runtime_error("sceNetCtlGetInfoV6: info code " + std::to_string(code) + " is not implemented"); +} + void APS5_VABI sceNetCtlTerm(void) { std::lock_guard lock(g_callbackLock); for (auto& slot : g_callbacks) slot = {}; @@ -506,4 +522,352 @@ 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(int event_type, int* error_code) { + if (!error_code) return SCE_NET_CTL_ERROR_INVALID_ADDR; + if (event_type < NET_CTL_EVENT_DISCONNECTED || event_type > NET_CTL_EVENT_IPOBTAINED) { + throw std::runtime_error("sceNetCtlGetResultV6: event type " + std::to_string(event_type) + " is not implemented"); + } + *error_code = 0; + 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) { + return 0; +} + +int APS5_VABI sceNetCtlApRpClearEvent(void) { + return 0; +} + +int APS5_VABI sceNetCtlApRpRegisterCallback(void) { + return 0; +} + +int APS5_VABI sceNetCtlApRpUnregisterCallback(void) { + return 0; +} + +int APS5_VABI sceNetCtlApRpGetResult(void) { + return 0; +} + +int APS5_VABI sceNetCtlApRpGetState(void) { + return 0; +} + +int APS5_VABI sceNetCtlApRpGetInfo(void) { + return 0; +} + +int APS5_VABI sceNetCtlApRpStart(void) { + 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) { + return 0; +} + +int APS5_VABI sceNetCtlApRpStartConf(void) { + return 0; +} + +int APS5_VABI sceNetCtlApRpStop(void) { + 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.native/Export.cpp b/core/libs/prx/libSceNgs2.native/Export.cpp new file mode 100644 index 0000000000..984f3782fe --- /dev/null +++ b/core/libs/prx/libSceNgs2.native/Export.cpp @@ -0,0 +1,83 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceNgs2JobSchedulerResetOption(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNgs2ModuleArrayEnumItems(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNgs2ModuleEnumConfigs(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNgs2ModuleQueueEnumItems(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNgs2RackQueryInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNgs2RackRunCommands(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNgs2StreamCreate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNgs2StreamCreateWithAllocator(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNgs2StreamDestroy(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNgs2StreamQueryBufferSize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNgs2StreamQueryInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNgs2StreamResetOption(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNgs2StreamRunCommands(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNgs2SystemQueryInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNgs2SystemRunCommands(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceNgs2.native/Ngs2Library.cmake b/core/libs/prx/libSceNgs2.native/Ngs2Library.cmake index 877276b0b5..b89226f992 100644 --- a/core/libs/prx/libSceNgs2.native/Ngs2Library.cmake +++ b/core/libs/prx/libSceNgs2.native/Ngs2Library.cmake @@ -9,7 +9,6 @@ function(add_sce_ngs2_library target) ${ngs2Dir}/src/Render.cpp ${ngs2Dir}/src/Reverb.cpp ${ngs2Dir}/src/System.cpp - ${ngs2Dir}/src/Unimplemented.cpp ${ngs2Dir}/src/Voice.cpp ) target_include_directories(${target} PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src) diff --git a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp index 106ded9315..a604adba3d 100644 --- a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp +++ b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp @@ -750,4 +750,14 @@ struct Ngs2GeomAttribute { }; static_assert(sizeof(Ngs2GeomAttribute) == 308); +struct Ngs2ReportDataHeader { + std::size_t size; + Ngs2Handle handle; + std::uint32_t type; + std::int32_t result; +}; +static_assert(sizeof(Ngs2ReportDataHeader) == 24); + +using Ngs2ReportHandler = void (APS5_VABI *)(const Ngs2ReportDataHeader* data, std::uintptr_t user_data); + #endif diff --git a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp index e71d56b87c..adc9f3d665 100644 --- a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp +++ b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include "prx/libSceNgs2.native/include/Ngs2Types.hpp" @@ -158,12 +159,15 @@ struct Ngs2Rack { std::uint32_t maxGrainSamples = 0; std::uint32_t maxChannelWorks = 0; std::uint32_t maxInputs = 0; + std::uintptr_t userData = 0; Ngs2ContextBufferInfo bufferInfo{}; Ngs2BufferAllocator allocator{}; std::vector voices; std::vector userFx; std::vector> userFxCommon; std::vector customModuleIds; + std::thread::id lockOwner{}; + std::uint32_t lockDepth = 0; }; struct Ngs2System { @@ -172,6 +176,7 @@ struct Ngs2System { Ngs2BufferAllocator allocator{}; std::uint32_t uid = 0; std::int64_t renderCount = 0; + std::uint32_t renderDepth = 0; std::uintptr_t userData = 0; std::vector racks; }; @@ -179,6 +184,32 @@ struct Ngs2System { std::string Ngs2Hex(std::uint32_t value); const std::uint8_t* Ngs2StreamEnd(const Ngs2Voice& voice, const Ngs2Block& block); std::recursive_mutex& Ngs2Mutex(); + +class Ngs2Lock { +public: + Ngs2Lock(); + ~Ngs2Lock(); + Ngs2Lock(const Ngs2Lock&) = delete; + Ngs2Lock& operator=(const Ngs2Lock&) = delete; +}; + +class Ngs2Unlocked { +public: + Ngs2Unlocked(); + ~Ngs2Unlocked(); + Ngs2Unlocked(const Ngs2Unlocked&) = delete; + Ngs2Unlocked& operator=(const Ngs2Unlocked&) = delete; + +private: + std::uint32_t depth; +}; + +bool Ngs2ExclusiveElsewhere(); +bool Ngs2RenderingHere(const Ngs2System& system); +void Ngs2EnterExclusive(); +void Ngs2LeaveExclusive(std::uint32_t depth = 1); +void Ngs2WaitOnce(); +void Ngs2NotifyWaiters(); Ngs2System* Ngs2FindSystem(Ngs2Handle handle); template diff --git a/core/libs/prx/libSceNgs2.native/src/Rack.cpp b/core/libs/prx/libSceNgs2.native/src/Rack.cpp index 8261f9ad42..47897d3215 100644 --- a/core/libs/prx/libSceNgs2.native/src/Rack.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Rack.cpp @@ -141,9 +141,11 @@ static int CreateRack(Ngs2Handle systemHandle, std::uint32_t rackId, const RackO } int Ngs2DestroyRack(Ngs2Rack& rack, Ngs2ContextBufferInfo* outBufferInfo) { + if (rack.lockDepth != 0) Ngs2LeaveExclusive(rack.lockDepth); Ngs2CleanupUserFx(rack); auto& racks = rack.system->racks; std::erase(racks, &rack); + Ngs2NotifyWaiters(); for (auto* other : racks) { for (auto& voice : other->voices) { for (auto& port : voice.ports) { @@ -173,7 +175,7 @@ int APS5_VABI sceNgs2RackCreate(uintptr_t system_handle, uint32_t rack_id, const if (handle == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; const auto options = CheckedRackOption(rack_id, option); if (buffer_info == nullptr) APS5_INVALID_ARG_EX; - std::lock_guard lock(Ngs2Mutex()); + const Ngs2Lock lock; return CreateRack(system_handle, rack_id, options, *buffer_info, {}, handle); } @@ -181,7 +183,7 @@ int APS5_VABI sceNgs2RackCreateWithAllocator(uintptr_t system_handle, uint32_t r if (handle == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; const auto options = CheckedRackOption(rack_id, option); if (allocator == nullptr || allocator->alloc_handler == nullptr || allocator->free_handler == nullptr) APS5_INVALID_ARG_EX; - std::lock_guard lock(Ngs2Mutex()); + const Ngs2Lock lock; if (Ngs2FindSystem(system_handle) == nullptr) return SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE; Ngs2ContextBufferInfo bufferInfo{}; bufferInfo.host_buffer_size = sizeof(Ngs2Rack); @@ -192,15 +194,21 @@ int APS5_VABI sceNgs2RackCreateWithAllocator(uintptr_t system_handle, uint32_t r } int APS5_VABI sceNgs2RackDestroy(uintptr_t rack_handle, Ngs2ContextBufferInfo* buffer_info) { - std::lock_guard lock(Ngs2Mutex()); + const Ngs2Lock lock; if (buffer_info != nullptr) *buffer_info = {}; auto* rack = Ngs2FindRack(rack_handle); if (rack == nullptr) return SCE_NGS2_ERROR_INVALID_RACK_HANDLE; + while (rack != nullptr && Ngs2ExclusiveElsewhere()) { + Ngs2WaitOnce(); + rack = Ngs2FindRack(rack_handle); + } + if (rack == nullptr) return SCE_NGS2_ERROR_INVALID_RACK_HANDLE; + if (Ngs2RenderingHere(*rack->system)) throw std::runtime_error("NGS2: destroying a rack of a system from that system's render (a voice callback) is not implemented"); return Ngs2DestroyRack(*rack, buffer_info); } int APS5_VABI sceNgs2RackGetVoiceHandle(uintptr_t rack_handle, uint32_t voice_id, uintptr_t* handle) { - std::lock_guard lock(Ngs2Mutex()); + const Ngs2Lock lock; auto* rack = Ngs2FindRack(rack_handle); if (rack == nullptr) return SCE_NGS2_ERROR_INVALID_RACK_HANDLE; if (handle == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; @@ -212,7 +220,7 @@ int APS5_VABI sceNgs2RackGetVoiceHandle(uintptr_t rack_handle, uint32_t voice_id int APS5_VABI sceNgs2RackGetInfo(uintptr_t rack_handle, Ngs2RackInfo* info, size_t info_size) { if (info == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; if (info_size != sizeof(Ngs2RackInfo)) return SCE_NGS2_ERROR_INVALID_OUT_SIZE; - std::lock_guard lock(Ngs2Mutex()); + const Ngs2Lock lock; const auto* rack = Ngs2FindRack(rack_handle); if (rack == nullptr) return SCE_NGS2_ERROR_INVALID_RACK_HANDLE; *info = {}; @@ -237,17 +245,44 @@ int APS5_VABI sceNgs2RackGetInfo(uintptr_t rack_handle, Ngs2RackInfo* info, size return SCE_NGS2_OK; } +int APS5_VABI sceNgs2RackSetUserData(uintptr_t rack_handle, uintptr_t user_data) { + std::lock_guard lock(Ngs2Mutex()); + auto* rack = Ngs2FindRack(rack_handle); + if (rack == nullptr) return SCE_NGS2_ERROR_INVALID_RACK_HANDLE; + rack->userData = user_data; + return SCE_NGS2_OK; +} + +int APS5_VABI sceNgs2RackGetUserData(uintptr_t rack_handle, uintptr_t* user_data) { + std::lock_guard lock(Ngs2Mutex()); + const auto* rack = Ngs2FindRack(rack_handle); + if (rack == nullptr) return SCE_NGS2_ERROR_INVALID_RACK_HANDLE; + if (user_data == nullptr) APS5_INVALID_ARG_EX; + *user_data = rack->userData; + return SCE_NGS2_OK; +} + int APS5_VABI sceNgs2RackLock(uintptr_t rack_handle) { - Ngs2Mutex().lock(); - if (Ngs2FindRack(rack_handle) != nullptr) return SCE_NGS2_OK; - Ngs2Mutex().unlock(); - return SCE_NGS2_ERROR_INVALID_RACK_HANDLE; + const Ngs2Lock lock; + const auto self = std::this_thread::get_id(); + auto* rack = Ngs2FindRack(rack_handle); + while (rack != nullptr && Ngs2ExclusiveElsewhere()) { + Ngs2WaitOnce(); + rack = Ngs2FindRack(rack_handle); + } + if (rack == nullptr) return SCE_NGS2_ERROR_INVALID_RACK_HANDLE; + Ngs2EnterExclusive(); + rack->lockOwner = self; + rack->lockDepth++; + return SCE_NGS2_OK; } int APS5_VABI sceNgs2RackUnlock(uintptr_t rack_handle) { - std::lock_guard lock(Ngs2Mutex()); - if (Ngs2FindRack(rack_handle) == nullptr) return SCE_NGS2_ERROR_INVALID_RACK_HANDLE; - Ngs2Mutex().unlock(); + const Ngs2Lock lock; + auto* rack = Ngs2FindRack(rack_handle); + if (rack == nullptr || rack->lockDepth == 0 || rack->lockOwner != std::this_thread::get_id()) return SCE_NGS2_ERROR_INVALID_RACK_HANDLE; + if (--rack->lockDepth == 0) rack->lockOwner = {}; + Ngs2LeaveExclusive(); return SCE_NGS2_OK; } diff --git a/core/libs/prx/libSceNgs2.native/src/Render.cpp b/core/libs/prx/libSceNgs2.native/src/Render.cpp index a3ec266e0a..fa764d375a 100644 --- a/core/libs/prx/libSceNgs2.native/src/Render.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Render.cpp @@ -78,7 +78,10 @@ bool Ngs2FinishBlock(Ngs2Voice& voice) { voice.blocks.pop_front(); } const auto revision = voice.waveformRevision; - if (voice.callback != nullptr && (voice.callbackFlags & info.flag) != 0) voice.callback(&info); + if (voice.callback != nullptr && (voice.callbackFlags & info.flag) != 0) { + const Ngs2Unlocked unlocked; + voice.callback(&info); + } return voice.waveformRevision == revision && voice.state == Ngs2PlayState::Playing; } diff --git a/core/libs/prx/libSceNgs2.native/src/System.cpp b/core/libs/prx/libSceNgs2.native/src/System.cpp index 17e696d525..99eede7a7c 100644 --- a/core/libs/prx/libSceNgs2.native/src/System.cpp +++ b/core/libs/prx/libSceNgs2.native/src/System.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include "prx/libc/include/General.hpp" @@ -22,6 +23,17 @@ static std::vector& Systems() { return *systems; } +struct Ngs2ReportRegistration { + std::uint32_t type; + Ngs2ReportHandler handler; + std::uintptr_t userData; +}; + +static std::vector& ReportRegistrations() { + static auto* const registrations = new std::vector(); + return *registrations; +} + std::string Ngs2Hex(std::uint32_t value) { char text[16]; std::snprintf(text, sizeof(text), "0x%x", value); @@ -33,6 +45,88 @@ std::recursive_mutex& Ngs2Mutex() { return *mutex; } +static std::uint32_t& LockDepth() { + thread_local std::uint32_t depth = 0; + return depth; +} + +static std::condition_variable_any& Ngs2Waiters() { + static auto* const waiters = new std::condition_variable_any(); + return *waiters; +} + +Ngs2Lock::Ngs2Lock() { + Ngs2Mutex().lock(); + ++LockDepth(); +} + +Ngs2Lock::~Ngs2Lock() { + --LockDepth(); + Ngs2Mutex().unlock(); +} + +Ngs2Unlocked::Ngs2Unlocked() : depth(std::exchange(LockDepth(), 0u)) { + for (std::uint32_t i = 0; i < depth; ++i) Ngs2Mutex().unlock(); +} + +Ngs2Unlocked::~Ngs2Unlocked() { + for (std::uint32_t i = 0; i < depth; ++i) Ngs2Mutex().lock(); + LockDepth() = depth; +} + +struct HeldNgs2Lock { + std::uint32_t depth = 0; + + void unlock() { + depth = std::exchange(LockDepth(), 0u); + for (std::uint32_t i = 0; i < depth; ++i) Ngs2Mutex().unlock(); + } + + void lock() { + for (std::uint32_t i = 0; i < depth; ++i) Ngs2Mutex().lock(); + LockDepth() = depth; + } +}; + +struct ExclusiveState { + std::thread::id owner; + std::uint32_t depth = 0; +}; + +static ExclusiveState& Exclusive() { + static ExclusiveState state; + return state; +} + +bool Ngs2RenderingHere(const Ngs2System& system) { + return system.renderDepth != 0; +} + +bool Ngs2ExclusiveElsewhere() { + return Exclusive().depth != 0 && Exclusive().owner != std::this_thread::get_id(); +} + +void Ngs2EnterExclusive() { + Exclusive().owner = std::this_thread::get_id(); + ++Exclusive().depth; +} + +void Ngs2LeaveExclusive(std::uint32_t depth) { + Exclusive().depth -= depth; + if (Exclusive().depth != 0) return; + Exclusive().owner = {}; + Ngs2NotifyWaiters(); +} + +void Ngs2WaitOnce() { + HeldNgs2Lock held; + Ngs2Waiters().wait(held); +} + +void Ngs2NotifyWaiters() { + Ngs2Waiters().notify_all(); +} + struct SystemLockState { std::thread::id owner; std::uint32_t depth = 0; @@ -162,7 +256,7 @@ int APS5_VABI sceNgs2SystemCreate(const Ngs2SystemOption* option, const Ngs2Cont if (handle == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; const auto checked = CheckedSystemOption(option); if (buffer_info == nullptr) APS5_INVALID_ARG_EX; - std::lock_guard lock(Ngs2Mutex()); + const Ngs2Lock lock; return CreateSystem(checked, *buffer_info, {}, handle); } @@ -170,15 +264,20 @@ int APS5_VABI sceNgs2SystemCreateWithAllocator(const Ngs2SystemOption* option, c if (handle == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; const auto checked = CheckedSystemOption(option); const auto bufferInfo = Allocate(allocator, sizeof(Ngs2System)); - std::lock_guard lock(Ngs2Mutex()); + const Ngs2Lock lock; return CreateSystem(checked, bufferInfo, *allocator, handle); } int APS5_VABI sceNgs2SystemDestroy(uintptr_t system_handle, Ngs2ContextBufferInfo* buffer_info) { - std::lock_guard lock(Ngs2Mutex()); + const Ngs2Lock lock; if (buffer_info != nullptr) *buffer_info = {}; auto* system = Ngs2FindSystem(system_handle); + while (system != nullptr && Ngs2ExclusiveElsewhere()) { + Ngs2WaitOnce(); + system = Ngs2FindSystem(system_handle); + } if (system == nullptr) return SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE; + if (Ngs2RenderingHere(*system)) throw std::runtime_error("NGS2: destroying a system from its own render (a voice callback) is not implemented"); while (!system->racks.empty()) { const int result = Ngs2DestroyRack(*system->racks.back(), nullptr); if (result != SCE_NGS2_OK) return result; @@ -198,7 +297,7 @@ int APS5_VABI sceNgs2SystemDestroy(uintptr_t system_handle, Ngs2ContextBufferInf int APS5_VABI sceNgs2SystemGetInfo(uintptr_t system_handle, Ngs2SystemInfo* info, size_t info_size) { if (info == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; if (info_size != sizeof(Ngs2SystemInfo)) return SCE_NGS2_ERROR_INVALID_OUT_SIZE; - std::lock_guard lock(Ngs2Mutex()); + const Ngs2Lock lock; const auto* system = Ngs2FindSystem(system_handle); if (system == nullptr) return SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE; *info = {}; @@ -217,7 +316,7 @@ int APS5_VABI sceNgs2SystemGetInfo(uintptr_t system_handle, Ngs2SystemInfo* info } int APS5_VABI sceNgs2SystemSetGrainSamples(uintptr_t system_handle, uint32_t num_samples) { - std::lock_guard lock(Ngs2Mutex()); + const Ngs2Lock lock; auto* system = Ngs2FindSystem(system_handle); if (system == nullptr) return SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE; if (num_samples == 0 || num_samples > system->option.max_grain_samples) APS5_INVALID_ARG_EX; @@ -226,7 +325,7 @@ int APS5_VABI sceNgs2SystemSetGrainSamples(uintptr_t system_handle, uint32_t num } int APS5_VABI sceNgs2SystemSetSampleRate(uintptr_t system_handle, uint32_t sample_rate) { - std::lock_guard lock(Ngs2Mutex()); + const Ngs2Lock lock; auto* system = Ngs2FindSystem(system_handle); if (system == nullptr) return SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE; if (sample_rate == 0) APS5_INVALID_ARG_EX; @@ -244,7 +343,7 @@ int APS5_VABI sceNgs2SystemSetSampleRate(uintptr_t system_handle, uint32_t sampl } int APS5_VABI sceNgs2SystemSetUserData(uintptr_t system_handle, uintptr_t user_data) { - std::lock_guard lock(Ngs2Mutex()); + const Ngs2Lock lock; auto* system = Ngs2FindSystem(system_handle); if (system == nullptr) return SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE; system->userData = user_data; @@ -252,7 +351,7 @@ int APS5_VABI sceNgs2SystemSetUserData(uintptr_t system_handle, uintptr_t user_d } int APS5_VABI sceNgs2SystemGetUserData(uintptr_t system_handle, uintptr_t* user_data) { - std::lock_guard lock(Ngs2Mutex()); + const Ngs2Lock lock; const auto* system = Ngs2FindSystem(system_handle); if (system == nullptr) return SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE; if (user_data == nullptr) APS5_INVALID_ARG_EX; @@ -284,8 +383,12 @@ int APS5_VABI sceNgs2SystemUnlock(uintptr_t system_handle) { } int APS5_VABI sceNgs2SystemRender(uintptr_t system_handle, const Ngs2RenderBufferInfo* buffer_info, uint32_t num_buffer_info) { - std::lock_guard lock(Ngs2Mutex()); + const Ngs2Lock lock; auto* system = Ngs2FindSystem(system_handle); + while (system != nullptr && Ngs2ExclusiveElsewhere()) { + Ngs2WaitOnce(); + system = Ngs2FindSystem(system_handle); + } if (system == nullptr) return SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE; if (buffer_info == nullptr || num_buffer_info == 0) APS5_INVALID_ARG_EX; SystemLockState* state = nullptr; @@ -301,13 +404,19 @@ int APS5_VABI sceNgs2SystemRender(uintptr_t system_handle, const Ngs2RenderBuffe found.depth++; state = &found; } + Ngs2EnterExclusive(); + system->renderDepth++; struct Release { SystemLockState* state; + Ngs2System* system; ~Release() { + system->renderDepth--; + Ngs2LeaveExclusive(); + Ngs2NotifyWaiters(); std::lock_guard locks(SystemLockMutex()); ReleaseSystemLock(*state); } - } release{state}; + } release{state, system}; Ngs2RenderSystem(*system, buffer_info, num_buffer_info); return SCE_NGS2_OK; } @@ -318,6 +427,27 @@ int APS5_VABI sceNgs2SystemResetOption(Ngs2SystemOption* option) { return SCE_NGS2_OK; } +int APS5_VABI sceNgs2ReportRegisterHandler(uint32_t report_type, Ngs2ReportHandler handler, uintptr_t user_data, Ngs2Handle* out_handle) { + if (out_handle == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; + if (handler == nullptr) APS5_INVALID_ARG_EX; + const Ngs2Lock lock; + auto* registration = new Ngs2ReportRegistration{report_type, handler, user_data}; + ReportRegistrations().push_back(registration); + *out_handle = reinterpret_cast(registration); + return SCE_NGS2_OK; +} + +int APS5_VABI sceNgs2ReportUnregisterHandler(Ngs2Handle report_handle) { + const Ngs2Lock lock; + auto& registrations = ReportRegistrations(); + const auto found = std::find(registrations.begin(), registrations.end(), reinterpret_cast(report_handle)); + if (found == registrations.end()) throw std::runtime_error("NGS2: unregistering a report handler that is not registered"); + Ngs2ReportRegistration* registration = *found; + registrations.erase(found); + delete registration; + return SCE_NGS2_OK; +} + } #pragma GCC visibility pop diff --git a/core/libs/prx/libSceNgs2.native/src/Unimplemented.cpp b/core/libs/prx/libSceNgs2.native/src/Unimplemented.cpp deleted file mode 100644 index 0cd8a3dd82..0000000000 --- a/core/libs/prx/libSceNgs2.native/src/Unimplemented.cpp +++ /dev/null @@ -1,28 +0,0 @@ -#include -#include - -#include "prx/libc/include/General.hpp" -#include "prx/libSceNgs2.native/include/Ngs2Types.hpp" - -#pragma GCC visibility push(default) - -extern "C" { - -int APS5_VABI sceNgs2ReportRegisterHandler(uint32_t report_type, void* handler, uintptr_t user_data, Ngs2Handle* out_handle) { - (void)report_type; - (void)handler; - (void)user_data; - (void)out_handle; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNgs2ReportUnregisterHandler(Ngs2Handle report_handle) { - (void)report_handle; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -} - -#pragma GCC visibility pop diff --git a/core/libs/prx/libSceNgs2.native/src/Voice.cpp b/core/libs/prx/libSceNgs2.native/src/Voice.cpp index 65df5e8364..9cde79710f 100644 --- a/core/libs/prx/libSceNgs2.native/src/Voice.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Voice.cpp @@ -69,6 +69,9 @@ void Ngs2Voice::ResetSetup() { userFxHandler = nullptr; userFxData = {}; userFxFlags = 0; + callback = nullptr; + callbackData = 0; + callbackFlags = 0; } const std::uint8_t* Ngs2StreamEnd(const Ngs2Voice& voice, const Ngs2Block& block) { @@ -393,7 +396,7 @@ extern "C" { int APS5_VABI sceNgs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamHeader* param_list) { if (param_list == nullptr) return SCE_NGS2_ERROR_NULL_PARAM_LIST; - std::lock_guard lock(Ngs2Mutex()); + const Ngs2Lock lock; auto* found = Ngs2FindVoice(voice_handle); if (found == nullptr) return SCE_NGS2_ERROR_INVALID_VOICE_HANDLE; auto& voice = *found; @@ -405,7 +408,7 @@ int APS5_VABI sceNgs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamHe } int APS5_VABI sceNgs2VoiceRunCommands(uintptr_t voice_handle, const Ngs2VoiceCommand* commands, size_t num_commands) { - std::lock_guard lock(Ngs2Mutex()); + const Ngs2Lock lock; auto* found = Ngs2FindVoice(voice_handle); if (found == nullptr) return SCE_NGS2_ERROR_INVALID_VOICE_HANDLE; auto& voice = *found; @@ -429,7 +432,7 @@ int APS5_VABI sceNgs2VoiceRunCommands(uintptr_t voice_handle, const Ngs2VoiceCom } int APS5_VABI sceNgs2VoiceGetState(uintptr_t voice_handle, Ngs2VoiceState* state, size_t state_size) { - std::lock_guard lock(Ngs2Mutex()); + const Ngs2Lock lock; auto* found = Ngs2FindVoice(voice_handle); if (found == nullptr) return SCE_NGS2_ERROR_INVALID_VOICE_HANDLE; if (state_size < sizeof(Ngs2VoiceState)) return SCE_NGS2_ERROR_INVALID_OUT_SIZE; @@ -476,7 +479,7 @@ int APS5_VABI sceNgs2VoiceGetState(uintptr_t voice_handle, Ngs2VoiceState* state int APS5_VABI sceNgs2VoiceGetPortInfo(uintptr_t voice_handle, uint32_t port, Ngs2VoicePortInfo* info, size_t info_size) { if (info == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; if (info_size != sizeof(Ngs2VoicePortInfo)) return SCE_NGS2_ERROR_INVALID_OUT_SIZE; - std::lock_guard lock(Ngs2Mutex()); + const Ngs2Lock lock; auto* found = Ngs2FindVoice(voice_handle); if (found == nullptr) return SCE_NGS2_ERROR_INVALID_VOICE_HANDLE; auto& voice = *found; @@ -490,7 +493,7 @@ int APS5_VABI sceNgs2VoiceGetPortInfo(uintptr_t voice_handle, uint32_t port, Ngs int APS5_VABI sceNgs2VoiceQueryInfo(uintptr_t voice_handle, uint32_t info_id, void* info, size_t info_size) { if (info == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; - std::lock_guard lock(Ngs2Mutex()); + const Ngs2Lock lock; auto* found = Ngs2FindVoice(voice_handle); if (found == nullptr) return SCE_NGS2_ERROR_INVALID_VOICE_HANDLE; const auto& voice = *found; @@ -506,7 +509,7 @@ int APS5_VABI sceNgs2VoiceQueryInfo(uintptr_t voice_handle, uint32_t info_id, vo int APS5_VABI sceNgs2VoiceGetStateFlags(uintptr_t voice_handle, uint32_t* state_flags) { if (state_flags == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; - std::lock_guard lock(Ngs2Mutex()); + const Ngs2Lock lock; const auto* found = Ngs2FindVoice(voice_handle); if (found == nullptr) return SCE_NGS2_ERROR_INVALID_VOICE_HANDLE; *state_flags = found->stateFlags; diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp index 15f0186700..4186d67946 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp @@ -9,11 +9,14 @@ #include #include #include +#include #include #include -static uintptr_t Sampler(uintptr_t system, const std::vector& pcm, std::uint32_t repeats) { - const auto voice = Voice(CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER)); +static uintptr_t Sampler(uintptr_t system, const std::vector& pcm, std::uint32_t repeats, uintptr_t* rack = nullptr) { + const auto created = CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER); + if (rack != nullptr) *rack = created; + const auto voice = Voice(created); Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, Ngs2SamplerVoiceSetupParam{{}, {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 1, 48000, 0, 0, 0}}); const Ngs2WaveformBlock block{0, pcm.size() * sizeof(std::int16_t), repeats, 0, static_cast(pcm.size()), 0, 0x55}; Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, pcm.data(), 0, 1, &block}); @@ -297,6 +300,35 @@ static void TestPitchAndRepeat() { Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } +static void TestSetupClearsCallback() { + const auto system = CreateSystem(); + const auto master = Mastering(system, 1); + const std::vector pcm{1000, 2000, 3000, 4000}; + const auto sampler = Sampler(system, pcm, 0); + Control(sampler, SCE_NGS2_VOICE_PARAM_CALLBACK, Ngs2VoiceCallbackParam{{}, OnBlock, 7, SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_END, 0}); + Control(sampler, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, Ngs2SamplerVoiceSetupParam{{}, {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 1, 48000, 0, 0, 0}}); + const Ngs2WaveformBlock oneShot{0, pcm.size() * sizeof(std::int16_t), 0, 0, static_cast(pcm.size()), 0, 0}; + Control(sampler, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, pcm.data(), 0, 1, &oneShot}); + Patch(sampler, master); + Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); + + callbackFlags.clear(); + auto out = RenderI16(system); + for (std::uint32_t i = 0; i < Grain; i++) Require(out[i] == (i < pcm.size() ? pcm[i] : 0)); + Require(callbackFlags.empty() && Flags(sampler) == 0); + + Control(sampler, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, Ngs2SamplerVoiceSetupParam{{}, {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 1, 48000, 0, 0, 0}}); + Patch(sampler, master); + Control(sampler, SCE_NGS2_VOICE_PARAM_CALLBACK, Ngs2VoiceCallbackParam{{}, OnBlock, 7, SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_END, 0}); + const Ngs2WaveformBlock stream{0, pcm.size() * sizeof(std::int16_t), 0, 0, static_cast(pcm.size()), 0, 0x55}; + Control(sampler, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, pcm.data(), 0, 1, &stream}); + Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); + out = RenderI16(system); + for (std::uint32_t i = 0; i < Grain; i++) Require(out[i] == (i < pcm.size() ? pcm[i] : 0)); + Require(callbackFlags.size() == 1 && callbackFlags[0] == SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_END); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + struct PcmResetCallback { std::vector pcm; std::uint32_t flag; @@ -733,6 +765,33 @@ static void TestUserData() { threw = true; } Require(threw); + + Require(sceNgs2RackSetUserData(0x1234, 5) == SCE_NGS2_ERROR_INVALID_RACK_HANDLE); + Require(sceNgs2RackSetUserData(system, 5) == SCE_NGS2_ERROR_INVALID_RACK_HANDLE); + value = 1; + Require(sceNgs2RackGetUserData(0x1234, &value) == SCE_NGS2_ERROR_INVALID_RACK_HANDLE && value == 1); + Require(sceNgs2RackGetUserData(system, &value) == SCE_NGS2_ERROR_INVALID_RACK_HANDLE && value == 1); + Require(sceNgs2RackGetUserData(0x1234, nullptr) == SCE_NGS2_ERROR_INVALID_RACK_HANDLE); + const auto rack = CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER); + const auto other = CreateRack(system, SCE_NGS2_RACK_ID_MASTERING); + Require(sceNgs2RackGetUserData(rack, &value) == SCE_NGS2_OK && value == 0); + Require(sceNgs2RackSetUserData(rack, 0x0123456789abcdef) == SCE_NGS2_OK); + Require(sceNgs2RackGetUserData(rack, &value) == SCE_NGS2_OK && value == 0x0123456789abcdef); + Require(sceNgs2RackGetUserData(other, &value) == SCE_NGS2_OK && value == 0); + Require(sceNgs2SystemGetUserData(system, &value) == SCE_NGS2_OK && value == 0xfedcba9876543210); + Require(sceNgs2RackSetUserData(other, 0) == SCE_NGS2_OK); + Require(sceNgs2RackGetUserData(rack, &value) == SCE_NGS2_OK && value == 0x0123456789abcdef); + threw = false; + try { + sceNgs2RackGetUserData(rack, nullptr); + } catch (const std::invalid_argument&) { + threw = true; + } + Require(threw); + Require(sceNgs2RackDestroy(rack, nullptr) == SCE_NGS2_OK); + value = 1; + Require(sceNgs2RackGetUserData(rack, &value) == SCE_NGS2_ERROR_INVALID_RACK_HANDLE && value == 1); + Require(sceNgs2RackSetUserData(rack, 5) == SCE_NGS2_ERROR_INVALID_RACK_HANDLE); Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } @@ -879,6 +938,282 @@ static void TestStereoIntoSurround() { Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } +static bool AnsweredWithin(const std::atomic& answered, std::chrono::milliseconds limit = std::chrono::seconds(2)) { + const auto deadline = std::chrono::steady_clock::now() + limit; + while (!answered && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + return answered; +} + +static void TestRackLockLeavesQueries() { + const auto system = CreateSystem(); + const auto rack = CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER); + const auto voice = Voice(rack); + Require(sceNgs2RackLock(0x1234) == SCE_NGS2_ERROR_INVALID_RACK_HANDLE); + Require(sceNgs2RackLock(rack) == SCE_NGS2_OK); + Require(sceNgs2RackLock(rack) == SCE_NGS2_OK); + std::atomic answered = false; + std::thread query([&] { + std::uint32_t flags = 0; + Require(sceNgs2VoiceGetStateFlags(voice, &flags) == SCE_NGS2_OK); + answered = true; + }); + const bool answeredWhileLocked = AnsweredWithin(answered); + std::atomic rendered = false; + std::thread render([&] { + RenderI16(system); + rendered = true; + }); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + const bool renderedWhileLocked = rendered; + Require(sceNgs2RackUnlock(rack) == SCE_NGS2_OK); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + const bool renderedWhileLockedOnce = rendered; + Require(sceNgs2RackUnlock(rack) == SCE_NGS2_OK); + query.join(); + render.join(); + Require(answeredWhileLocked); + Require(!renderedWhileLocked && !renderedWhileLockedOnce && rendered); + Require(sceNgs2RackUnlock(rack) == SCE_NGS2_ERROR_INVALID_RACK_HANDLE); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + +struct QueryInCallback { + uintptr_t voice = 0; + std::atomic answered = false; + bool answeredInCallback = false; + std::thread query; +}; + +static void APS5_VABI QueryFromAnotherThread(const Ngs2VoiceCallbackInfo* info) { + auto& callback = *reinterpret_cast(info->callback_data); + callback.query = std::thread([&callback] { + std::uint32_t flags = 0; + Require(sceNgs2VoiceGetStateFlags(callback.voice, &flags) == SCE_NGS2_OK); + callback.answered = true; + }); + callback.answeredInCallback = AnsweredWithin(callback.answered); +} + +static void TestVoiceCallbackLeavesQueries() { + const auto system = CreateSystem(); + const auto master = Mastering(system, 1); + const std::vector pcm(Grain / 2, 1000); + const auto sampler = Sampler(system, pcm, 0); + Patch(sampler, master); + QueryInCallback callback; + callback.voice = sampler; + Control(sampler, SCE_NGS2_VOICE_PARAM_CALLBACK, Ngs2VoiceCallbackParam{{}, QueryFromAnotherThread, reinterpret_cast(&callback), SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_END, 0}); + Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); + RenderI16(system); + Require(callback.query.joinable()); + callback.query.join(); + Require(callback.answered && callback.answeredInCallback); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + +struct RackLockInCallback { + uintptr_t rack = 0; + std::atomic locked = false; + std::atomic callbackReturned = false; + std::atomic release = false; + bool lockedDuringCallback = false; + std::thread locker; +}; + +static void APS5_VABI LockFromAnotherThread(const Ngs2VoiceCallbackInfo* info) { + auto& callback = *reinterpret_cast(info->callback_data); + callback.locker = std::thread([&callback] { + Require(sceNgs2RackLock(callback.rack) == SCE_NGS2_OK); + callback.lockedDuringCallback = !callback.callbackReturned; + callback.locked = true; + while (!callback.release) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + Require(sceNgs2RackUnlock(callback.rack) == SCE_NGS2_OK); + }); + static_cast(AnsweredWithin(callback.locked, std::chrono::milliseconds(200))); + callback.callbackReturned = true; +} + +static void TestRackLockWaitsForRender() { + const auto system = CreateSystem(); + const auto master = Mastering(system, 1); + const std::vector pcm(Grain / 2, 1000); + RackLockInCallback callback; + const auto sampler = Sampler(system, pcm, 0, &callback.rack); + Patch(sampler, master); + Control(sampler, SCE_NGS2_VOICE_PARAM_CALLBACK, Ngs2VoiceCallbackParam{{}, LockFromAnotherThread, reinterpret_cast(&callback), SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_END, 0}); + Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); + RenderI16(system); + const bool lockedAfterRender = AnsweredWithin(callback.locked); + callback.release = true; + Require(callback.locker.joinable()); + callback.locker.join(); + Require(lockedAfterRender && !callback.lockedDuringCallback); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + +struct CrossSystemCallback { + uintptr_t otherRack = 0; + std::atomic* entered = nullptr; + const std::atomic* awaited = nullptr; +}; + +static void APS5_VABI LockAnotherSystem(const Ngs2VoiceCallbackInfo* info) { + auto& callback = *reinterpret_cast(info->callback_data); + *callback.entered = true; + static_cast(AnsweredWithin(*callback.awaited, std::chrono::milliseconds(200))); + Require(sceNgs2RackLock(callback.otherRack) == SCE_NGS2_OK); + Require(sceNgs2RackUnlock(callback.otherRack) == SCE_NGS2_OK); +} + +static uintptr_t CallbackSampler(uintptr_t system, CrossSystemCallback& callback, uintptr_t* rack) { + static const std::vector pcm(Grain / 2, 1000); + const auto sampler = Sampler(system, pcm, 0, rack); + Patch(sampler, Mastering(system, 1)); + Control(sampler, SCE_NGS2_VOICE_PARAM_CALLBACK, Ngs2VoiceCallbackParam{{}, LockAnotherSystem, reinterpret_cast(&callback), SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_END, 0}); + Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); + return sampler; +} + +static void TestCallbacksLockEachOthersSystem() { + const auto first = CreateSystem(); + const auto second = CreateSystem(); + std::atomic firstEntered = false; + std::atomic secondEntered = false; + CrossSystemCallback firstCallback{0, &firstEntered, &secondEntered}; + CrossSystemCallback secondCallback{0, &secondEntered, &firstEntered}; + uintptr_t firstRack = 0; + uintptr_t secondRack = 0; + CallbackSampler(first, firstCallback, &firstRack); + CallbackSampler(second, secondCallback, &secondRack); + firstCallback.otherRack = secondRack; + secondCallback.otherRack = firstRack; + std::atomic firstRendered = false; + std::atomic secondRendered = false; + std::thread firstRender([&] { + RenderI16(first); + firstRendered = true; + }); + std::thread secondRender([&] { + RenderI16(second); + secondRendered = true; + }); + Require(AnsweredWithin(firstRendered) && AnsweredWithin(secondRendered)); + firstRender.join(); + secondRender.join(); + Require(firstEntered && secondEntered); + Require(sceNgs2SystemDestroy(first, nullptr) == SCE_NGS2_OK); + Require(sceNgs2SystemDestroy(second, nullptr) == SCE_NGS2_OK); +} + +static void TestRackLocksAcrossSystemsAndRender() { + const auto first = CreateSystem(); + const auto second = CreateSystem(); + const auto heldRack = CreateRack(second, SCE_NGS2_RACK_ID_SAMPLER); + std::atomic entered = false; + const std::atomic never = false; + CrossSystemCallback callback{heldRack, &entered, &never}; + uintptr_t renderedRack = 0; + CallbackSampler(first, callback, &renderedRack); + std::atomic holding = false; + std::atomic released = false; + std::thread locker([&] { + Require(sceNgs2RackLock(heldRack) == SCE_NGS2_OK); + holding = true; + static_cast(AnsweredWithin(entered, std::chrono::milliseconds(200))); + Require(sceNgs2RackLock(renderedRack) == SCE_NGS2_OK); + Require(sceNgs2RackUnlock(renderedRack) == SCE_NGS2_OK); + Require(sceNgs2RackUnlock(heldRack) == SCE_NGS2_OK); + released = true; + }); + Require(AnsweredWithin(holding)); + std::atomic rendered = false; + std::thread render([&] { + RenderI16(first); + rendered = true; + }); + Require(AnsweredWithin(released) && AnsweredWithin(rendered)); + locker.join(); + render.join(); + Require(entered); + Require(sceNgs2SystemDestroy(first, nullptr) == SCE_NGS2_OK); + Require(sceNgs2SystemDestroy(second, nullptr) == SCE_NGS2_OK); +} + +static void TestRackDestroyWaitsForLock() { + const auto system = CreateSystem(); + const auto rack = CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER); + Require(sceNgs2RackLock(rack) == SCE_NGS2_OK); + std::atomic destroyed = false; + std::thread destroyer([&] { + Require(sceNgs2RackDestroy(rack, nullptr) == SCE_NGS2_OK); + destroyed = true; + }); + const bool destroyedWhileLocked = AnsweredWithin(destroyed, std::chrono::milliseconds(200)); + Require(!destroyedWhileLocked && sceNgs2RackUnlock(rack) == SCE_NGS2_OK); + Require(AnsweredWithin(destroyed)); + destroyer.join(); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + +struct DestroyInCallback { + uintptr_t handle = 0; + bool system = false; +}; + +static void APS5_VABI DestroyFromCallback(const Ngs2VoiceCallbackInfo* info) { + const auto& destroy = *reinterpret_cast(info->callback_data); + static_cast(destroy.system ? sceNgs2SystemDestroy(destroy.handle, nullptr) : sceNgs2RackDestroy(destroy.handle, nullptr)); +} + +static void APS5_VABI RenderFromCallback(const Ngs2VoiceCallbackInfo* info) { + static_cast(RenderI16(info->callback_data)); +} + +static void TestDestroyDuringOwnRenderThrows() { + for (const bool destroySystem : {false, true}) { + const auto system = CreateSystem(); + uintptr_t rack = 0; + const std::vector pcm(Grain / 2, 1000); + const auto sampler = Sampler(system, pcm, 0, &rack); + Patch(sampler, Mastering(system, 1)); + const DestroyInCallback destroy{destroySystem ? system : rack, destroySystem}; + Control(sampler, SCE_NGS2_VOICE_PARAM_CALLBACK, Ngs2VoiceCallbackParam{{}, DestroyFromCallback, reinterpret_cast(&destroy), SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_END, 0}); + Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); + bool refused = false; + try { + RenderI16(system); + } catch (const std::runtime_error& error) { + refused = std::string(error.what()).find("is not implemented") != std::string::npos; + } + Require(refused); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); + } + for (const bool destroySystem : {false, true}) { + const auto outer = CreateSystem(); + const auto inner = CreateSystem(); + uintptr_t outerRack = 0; + const std::vector pcm(Grain / 2, 1000); + const auto outerSampler = Sampler(outer, pcm, 0, &outerRack); + Patch(outerSampler, Mastering(outer, 1)); + const auto innerSampler = Sampler(inner, pcm, 0); + Patch(innerSampler, Mastering(inner, 1)); + const DestroyInCallback destroy{destroySystem ? outer : outerRack, destroySystem}; + Control(outerSampler, SCE_NGS2_VOICE_PARAM_CALLBACK, Ngs2VoiceCallbackParam{{}, RenderFromCallback, inner, SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_END, 0}); + Control(innerSampler, SCE_NGS2_VOICE_PARAM_CALLBACK, Ngs2VoiceCallbackParam{{}, DestroyFromCallback, reinterpret_cast(&destroy), SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_END, 0}); + Event(outerSampler, SCE_NGS2_VOICE_EVENT_PLAY); + Event(innerSampler, SCE_NGS2_VOICE_EVENT_PLAY); + bool refused = false; + try { + RenderI16(outer); + } catch (const std::runtime_error& error) { + refused = std::string(error.what()).find("is not implemented") != std::string::npos; + } + Require(refused); + Require(sceNgs2SystemDestroy(inner, nullptr) == SCE_NGS2_OK); + Require(sceNgs2SystemDestroy(outer, nullptr) == SCE_NGS2_OK); + } +} + static void TestLock() { const auto system = CreateSystem(); const auto voice = Voice(CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER)); @@ -1135,10 +1470,18 @@ int main() { TestPcmCallbackReset(); TestPcmCallbackRefill(); TestPcmCallbackSetup(); + TestSetupClearsCallback(); TestMasteringGain(); TestMatrixLevelClamp(); TestStereoIntoSurround(); TestLock(); + TestRackLockLeavesQueries(); + TestVoiceCallbackLeavesQueries(); + TestRackLockWaitsForRender(); + TestCallbacksLockEachOthersSystem(); + TestRackLocksAcrossSystemsAndRender(); + TestRackDestroyWaitsForLock(); + TestDestroyDuringOwnRenderThrows(); TestDefaultChannelMap(); TestUnsetMatrix(); TestRenderCapacity(SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 1); diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Report.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Report.cpp index f76d61f46e..146b691a34 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Report.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Report.cpp @@ -1,33 +1,43 @@ -#include "prx/libc/include/general/VabiMacros.hpp" +#include "Ngs2Test.hpp" #include -#include #include -extern "C" { -int APS5_VABI sceNgs2ReportRegisterHandler(std::uint32_t reportType, void* handler, std::uintptr_t userData, std::uintptr_t* outHandle); -int APS5_VABI sceNgs2ReportUnregisterHandler(std::uintptr_t reportHandle); -} - namespace { -void Require(bool value) { if (!value) std::abort(); } - template bool Throws(Call call) { try { call(); - } catch (const std::runtime_error&) { + } catch (const std::exception&) { return true; } return false; } +int callbacks = 0; +void APS5_VABI Counted(const Ngs2ReportDataHeader*, std::uintptr_t) { + callbacks++; +} + } int main() { - std::uintptr_t handle = 0x1234; + Ngs2Handle handle = 0x1234; Require(Throws([&] { sceNgs2ReportRegisterHandler(5, nullptr, 0, &handle); })); Require(handle == 0x1234); + Require(sceNgs2ReportRegisterHandler(5, Counted, 0, nullptr) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + + Require(sceNgs2ReportRegisterHandler(5, Counted, 42, &handle) == SCE_NGS2_OK); + Require(handle != 0); + Ngs2Handle second = 0; + Require(sceNgs2ReportRegisterHandler(7, Counted, 43, &second) == SCE_NGS2_OK); + Require(second != 0 && second != handle); + + Require(sceNgs2ReportUnregisterHandler(handle) == SCE_NGS2_OK); + Require(Throws([&] { sceNgs2ReportUnregisterHandler(handle); })); Require(Throws([] { sceNgs2ReportUnregisterHandler(0x1234); })); + Require(sceNgs2ReportUnregisterHandler(second) == SCE_NGS2_OK); + Require(callbacks == 0); + return 0; } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp index 4cc96707d1..e861171262 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp @@ -28,11 +28,15 @@ int APS5_VABI sceNgs2SystemRender(uintptr_t, const Ngs2RenderBufferInfo*, uint32 int APS5_VABI sceNgs2PanInit(Ngs2PanWork*, const float*, float, uint32_t); int APS5_VABI sceNgs2PanGetVolumeMatrix(Ngs2PanWork*, const Ngs2PanParam*, uint32_t, uint32_t, float*); int APS5_VABI sceNgs2RackQueryBufferSize(uint32_t, const Ngs2RackOption*, Ngs2ContextBufferInfo*); +int APS5_VABI sceNgs2RackLock(uintptr_t); +int APS5_VABI sceNgs2RackUnlock(uintptr_t); int APS5_VABI sceNgs2RackCreate(uintptr_t, uint32_t, const Ngs2RackOption*, const Ngs2ContextBufferInfo*, uintptr_t*); int APS5_VABI sceNgs2RackCreateWithAllocator(uintptr_t, uint32_t, const Ngs2RackOption*, const Ngs2BufferAllocator*, uintptr_t*); int APS5_VABI sceNgs2RackDestroy(uintptr_t, Ngs2ContextBufferInfo*); int APS5_VABI sceNgs2RackGetVoiceHandle(uintptr_t, uint32_t, uintptr_t*); int APS5_VABI sceNgs2RackGetInfo(uintptr_t, Ngs2RackInfo*, size_t); +int APS5_VABI sceNgs2RackSetUserData(uintptr_t, uintptr_t); +int APS5_VABI sceNgs2RackGetUserData(uintptr_t, uintptr_t*); int APS5_VABI sceNgs2VoiceControl(uintptr_t, const Ngs2VoiceParamHeader*); int APS5_VABI sceNgs2VoiceRunCommands(uintptr_t, const Ngs2VoiceCommand*, size_t); int APS5_VABI sceNgs2VoiceGetState(uintptr_t, Ngs2VoiceState*, size_t); @@ -43,6 +47,8 @@ int APS5_VABI sceNgs2GeomResetListenerParam(Ngs2GeomListenerParam*); int APS5_VABI sceNgs2GeomCalcListener(const Ngs2GeomListenerParam*, Ngs2GeomListenerWork*, std::uint32_t); int APS5_VABI sceNgs2GeomResetSourceParam(Ngs2GeomSourceParam*); int APS5_VABI sceNgs2GeomApply(const Ngs2GeomListenerWork*, const Ngs2GeomSourceParam*, Ngs2GeomAttribute*, std::uint32_t); +int APS5_VABI sceNgs2ReportRegisterHandler(std::uint32_t, Ngs2ReportHandler, std::uintptr_t, Ngs2Handle*); +int APS5_VABI sceNgs2ReportUnregisterHandler(Ngs2Handle); } inline void Check(bool value, int line) { 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..954ec4c20f --- /dev/null +++ b/core/libs/prx/libSceNgs2/PendingExports.cpp @@ -0,0 +1,49 @@ +#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 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/libSceNpAuthAuthorizedAppDialog/Export.cpp b/core/libs/prx/libSceNpAuthAuthorizedAppDialog/Export.cpp index 6a722de5a4..f1cbb534c2 100644 --- a/core/libs/prx/libSceNpAuthAuthorizedAppDialog/Export.cpp +++ b/core/libs/prx/libSceNpAuthAuthorizedAppDialog/Export.cpp @@ -59,6 +59,7 @@ int APS5_VABI sceNpAuthAuthorizedAppDialogGetResult(void* result) { int APS5_VABI sceNpAuthAuthorizedAppDialogClose(void) { if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + g_status = COMMON_DIALOG_STATUS_FINISHED; 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/libSceNpCommon/CMakeLists.txt b/core/libs/prx/libSceNpCommon/CMakeLists.txt new file mode 100644 index 0000000000..f85817c30e --- /dev/null +++ b/core/libs/prx/libSceNpCommon/CMakeLists.txt @@ -0,0 +1,6 @@ +add_library(libSceNpCommon SHARED EXCLUDE_FROM_ALL PendingExports.cpp) +if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp") + target_sources(libSceNpCommon PRIVATE Export.cpp) +endif() +target_include_directories(libSceNpCommon PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceNpCommon) diff --git a/core/libs/prx/libSceNpCommon/PendingExports.cpp b/core/libs/prx/libSceNpCommon/PendingExports.cpp new file mode 100644 index 0000000000..c9d6e53ffd --- /dev/null +++ b/core/libs/prx/libSceNpCommon/PendingExports.cpp @@ -0,0 +1,758 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceNpAllocateKernelMemoryNoAlignment(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpAllocateKernelMemoryWithAlignment(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpArchInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpArchTerm(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpAtomicCas32(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpAtomicDec32(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpAtomicInc32(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpBase64Decoder(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpBase64Encoder(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpBase64GetDecodeSize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpBase64UrlDecoder(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpBase64UrlEncoder(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpBase64UrlGetDecodeSize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpCalloutInitCtx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpCalloutStartOnCtx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpCalloutStartOnCtx64(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpCalloutStopOnCtx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpCalloutTermCtx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpCancelEventFlag(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpClearEventFlag(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpCloseEventFlag(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpCloseSema(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpCmpNpIdInOrder(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpCmpOnlineId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpCondDestroy(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpCondInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpCondSignal(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpCondSignalAll(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpCondSignalTo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpCondTimedwait(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpCondWait(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpCreateEventFlag(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpCreateSema(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpCreateThread(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpDbgAssignDebugId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpDbgDumpBinary(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpDbgDumpText(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpDeleteEventFlag(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpDeleteSema(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpFreeKernelMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpGetNavSdkVersion(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpGetPlatformType(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpGetProcessId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpGetRandom(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpGetSdkVersion(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpGetSdkVersionUInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpGetSystemClockUsec(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpGlobalHeapGetAllocator(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpGlobalHeapGetAllocatorEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpGlobalHeapGetAllocatorExPtr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpGlobalHeapGetAllocatorPtr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpHeapDestroy(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpHeapGetAllocator(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpHeapGetStat(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpHeapInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpHeapShowStat(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpHexToInt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpInt32ToStr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpInt64ToStr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpIntGetPlatformType(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpIntIsOnlineIdString(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpIntIsValidOnlineId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpIntSetPlatformType(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpIntToHex(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpIpc2ClientInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpIpc2ClientTerm(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpJoinThread(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpJsonParse(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpJsonParseBuf(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpJsonParseBufInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpJsonParseEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpJsonParseExInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpJsonParseInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpLwCondDestroy(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpLwCondInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpLwCondSignal(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpLwCondSignalAll(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpLwCondSignalTo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpLwCondWait(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpLwMutexDestroy(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpLwMutexInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpLwMutexLock(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpLwMutexTryLock(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpLwMutexUnlock(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMemoryHeapDestroy(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMemoryHeapGetAllocator(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMemoryHeapGetAllocatorEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMemoryHeapInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMutexDestroy(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMutexInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMutexLock(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMutexTryLock(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMutexUnlock(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpOpenEventFlag(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpOpenSema(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPanic(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPollEventFlag(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPollSema(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpRtcConvertToPosixTime(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpRtcFormatRFC3339(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpRtcParseRFC3339(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpServerErrorJsonGetErrorCode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpServerErrorJsonMultiGetErrorCode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpServerErrorJsonParse(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpServerErrorJsonParseInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpServerErrorJsonParseMultiInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSetEventFlag(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSetPlatformType(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSignalSema(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpStrBuildHex(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpStrParseHex(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpStrToInt32(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpStrToInt64(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpStrToUInt32(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpStrToUInt64(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpStrcpyToBuf(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpStrnParseHex(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpStrncpyToBuf(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpThreadGetId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUInt32ToStr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUInt64ToStr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUserGetUserIdList(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilBuildTitleId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilCanonicalizeNpIdForPs4(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilCanonicalizeNpIdForPsp2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilCmpAccountId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilGetDateSetAuto(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilGetDbgCommerce(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilGetEnv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilGetFakeDisplayNameMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilGetFakeRateLimit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilGetIgnoreNpTitleId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilGetNpDebug(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilGetNpLanguageCode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilGetNpLanguageCode2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilGetNpLanguageCode2Str(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilGetNpLanguageCodeStr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilGetNpTestPatch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilGetNthChar(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilGetShareTitleCheck(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilGetSystemLanguage(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilGetTrcNotify(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilGetWebApi2FakeRateLimit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilGetWebApi2FakeRateLimitTarget(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilGetWebTraceSetting(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilHttpUrlEncode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilJidToNpId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilJsonEscape(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUtilJsonGetOneChar(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} + +int APS5_VABI sceNpUtilJsonUnescape(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} diff --git a/core/libs/prx/libSceNpCppWebApi/CMakeLists.txt b/core/libs/prx/libSceNpCppWebApi/CMakeLists.txt new file mode 100644 index 0000000000..2519e8cc81 --- /dev/null +++ b/core/libs/prx/libSceNpCppWebApi/CMakeLists.txt @@ -0,0 +1,21 @@ +add_library(libSceNpCppWebApi SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceNpCppWebApi PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceNpCppWebApi) + +if(MINGW) + foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) + string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") + endforeach() +endif() + +if(BUILD_TESTING) + add_test_executable(guest_np_cpp_web_api_string_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpCppWebApiString.cpp) + target_include_directories(guest_np_cpp_web_api_string_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_np_cpp_web_api_string_tests PRIVATE libSceNpCppWebApi libc) + configure_windows_unwind(guest_np_cpp_web_api_string_tests) + add_test(NAME guest_np_cpp_web_api_string COMMAND guest_np_cpp_web_api_string_tests) + if(WIN32) + set_tests_properties(guest_np_cpp_web_api_string PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceNpCppWebApi/Export.cpp b/core/libs/prx/libSceNpCppWebApi/Export.cpp new file mode 100644 index 0000000000..6238d389f8 --- /dev/null +++ b/core/libs/prx/libSceNpCppWebApi/Export.cpp @@ -0,0 +1,1893 @@ +#include +#include +#include +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +static constexpr int SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN = static_cast(0x80552907); + +// Offline PSN: request entry points (API calls, transaction start/readData, factories) fail with +// SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN, the constant shadPS4/fpPS4 document for this code; parameter +// bookkeeping succeeds; response accessors stay unimplemented because a failed request never +// produces a response. +extern "C" { + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V111UserFactory6createEPNS1_6Common10LibContextEPNS5_12IntrusivePtrINS3_4UserEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V121GetRankingRequestBody18setStartSerialRankERKi() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V121GetRankingRequestBody8setGroupERKNS3_5GroupE() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V121GetRankingRequestBody8setLimitERKi() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V121GetRankingRequestBody8setUsersERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_4UserEEEEE() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V121GetRankingRequestBody9setOffsetERKi() { + return 0; +} + +void* APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V122GetRankingResponseBody10getEntriesEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V122RecordScoreRequestBody10setCommentEPKc() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V122RecordScoreRequestBody12setSmallDataEPKvm() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V122RecordScoreRequestBody15setNeedsTmpRankERKb() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V122RecordScoreRequestBody19setComparedDateTimeERK10SceRtcTick() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V122RecordScoreRequestBody7setPcIdERKi() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V128GetRankingRequestBodyFactory6createEPNS1_6Common10LibContextEPNS5_12IntrusivePtrINS3_21GetRankingRequestBodyEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V129RecordScoreRequestBodyFactory6createEPNS1_6Common10LibContextElPNS5_12IntrusivePtrINS3_22RecordScoreRequestBodyEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V14User12setAccountIdERKm() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V14User7setPcIdERKi() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V17ViewApi10getRankingEiRKNS4_21ParameterToGetRankingERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_22GetRankingResponseBodyEEENSA_INS8_18ResponseHeaderBaseEEEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V17ViewApi21ParameterToGetRanking10initializeEPNS1_6Common10LibContextEi() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V17ViewApi21ParameterToGetRanking24setgetRankingRequestBodyENS1_6Common12IntrusivePtrINS3_21GetRankingRequestBodyEEE() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V17ViewApi21ParameterToGetRanking9terminateEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V17ViewApi21ParameterToGetRankingC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V17ViewApi21ParameterToGetRankingD1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V17ViewApi22getLargeDataByObjectIdEiRKNS4_33ParameterToGetLargeDataByObjectIdERNS1_6Common21DownStreamTransactionINS8_12IntrusivePtrINS4_37GetLargeDataByObjectIdResponseHeadersEEEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V17ViewApi33ParameterToGetLargeDataByObjectId10initializeEPNS1_6Common10LibContextEPKc() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V17ViewApi33ParameterToGetLargeDataByObjectId9terminateEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V17ViewApi33ParameterToGetLargeDataByObjectIdC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V17ViewApi33ParameterToGetLargeDataByObjectIdD1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19BoardsApi18getBoardDefinitionEiRKNS4_29ParameterToGetBoardDefinitionERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_30GetBoardDefinitionResponseBodyEEENSA_INS8_18ResponseHeaderBaseEEEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19BoardsApi29ParameterToGetBoardDefinition10initializeEPNS1_6Common10LibContextEi() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19BoardsApi29ParameterToGetBoardDefinition9terminateEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19BoardsApi29ParameterToGetBoardDefinitionC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19BoardsApi29ParameterToGetBoardDefinitionD1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19RecordApi11recordScoreEiRKNS4_22ParameterToRecordScoreERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_23RecordScoreResponseBodyEEENSA_INS4_26RecordScoreResponseHeadersEEEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19RecordApi15recordLargeDataEiRKNS4_26ParameterToRecordLargeDataERNS1_6Common19UpStreamTransactionINS8_12IntrusivePtrINS3_27RecordLargeDataResponseBodyEEENSA_INS8_18ResponseHeaderBaseEEEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19RecordApi22ParameterToRecordScore10initializeEPNS1_6Common10LibContextEiNS6_12IntrusivePtrINS3_22RecordScoreRequestBodyEEE() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19RecordApi22ParameterToRecordScore22setxPsnAtomicOperationENS5_19XPsnAtomicOperationE() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19RecordApi22ParameterToRecordScore9terminateEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19RecordApi22ParameterToRecordScoreC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19RecordApi22ParameterToRecordScoreD1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19RecordApi26ParameterToRecordLargeData10initializeEPNS1_6Common10LibContextEiNS5_19XPsnAtomicOperationEPKc() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19RecordApi26ParameterToRecordLargeData9terminateEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19RecordApi26ParameterToRecordLargeDataC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19RecordApi26ParameterToRecordLargeDataD1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi12downloadDataEiRKNS4_23ParameterToDownloadDataERNS1_6Common21DownStreamTransactionINS8_12IntrusivePtrINS4_27DownloadDataResponseHeadersEEEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi23ParameterToDownloadData10initializeEPNS1_6Common10LibContextEPKci() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi23ParameterToDownloadData9terminateEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi23ParameterToDownloadDataC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi23ParameterToDownloadDataD1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common10InitParamsC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common10InitParamsD1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common10LibContextC1Ev(uint64_t* self) { + *self = 0; + return 0; +} + +// Initialization succeeds as on an offline console (see libSceJson2); individual requests fail. +int APS5_VABI _ZN3sce2Np9CppWebApi6Common10initializeERKNS2_10InitParamsERNS2_10LibContextE(const void* params, uint64_t* context) { + (void)params; + if (context) *context = 1; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEENS4_INS6_9RecordApi26RecordScoreResponseHeadersEEEEC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEENS4_INS6_9RecordApi26RecordScoreResponseHeadersEEEED1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void* self) { + (void)self; + return 0; +} + +struct IntrusivePtr { + void* object; + void (APS5_VABI* deleter)(void*); + void* libContext; +}; +static_assert(sizeof(IntrusivePtr) == 0x18); + +static constexpr std::size_t ReferenceCountOffset = 0; +static constexpr std::size_t VectorReferenceCountOffset = 0x30; + +static std::atomic_ref ReferenceCount(void* object, const std::size_t offset) { + return std::atomic_ref(*reinterpret_cast(static_cast(object) + offset)); +} + +static void CopyIntrusivePtr(IntrusivePtr* self, const IntrusivePtr* source, const std::size_t referenceCountOffset) { + *self = *source; + if (self->object) ReferenceCount(self->object, referenceCountOffset).fetch_add(1); +} + +static void ReleaseIntrusivePtr(IntrusivePtr* self, const std::size_t referenceCountOffset, const char* function) { + if (!self->object) return; + if (ReferenceCount(self->object, referenceCountOffset).fetch_sub(1) != 1) return; + if (!self->deleter) NotImplemented_nid_no_patch(function); + self->deleter(self->object); + self->object = nullptr; + self->deleter = nullptr; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEEC1ERS7_(IntrusivePtr* self, const IntrusivePtr* source) { + CopyIntrusivePtr(self, source, ReferenceCountOffset); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, ReferenceCountOffset, __func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, ReferenceCountOffset, __func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEC1ERS7_(IntrusivePtr* self, const IntrusivePtr* source) { + CopyIntrusivePtr(self, source, ReferenceCountOffset); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, ReferenceCountOffset, __func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, ReferenceCountOffset, __func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, ReferenceCountOffset, __func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V14UserEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V14UserEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, ReferenceCountOffset, __func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V19RecordApi26RecordScoreResponseHeadersEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V19RecordApi26RecordScoreResponseHeadersEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, ReferenceCountOffset, __func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_12Leaderboards2V15EntryEEEEEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, VectorReferenceCountOffset, __func__); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common13ConstIteratorINS2_12IntrusivePtrINS1_12Leaderboards2V15EntryEEEED2Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEENS4_INS6_9RecordApi26RecordScoreResponseHeadersEEEE5startEPNS2_10LibContextE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEENS4_INS6_9RecordApi26RecordScoreResponseHeadersEEEE6finishEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_12Leaderboards2V127RecordLargeDataResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common19UpStreamTransactionINS2_12IntrusivePtrINS1_12Leaderboards2V127RecordLargeDataResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextEm() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common19UpStreamTransactionINS2_12IntrusivePtrINS1_12Leaderboards2V127RecordLargeDataResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE8sendDataEPKvm() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common19UpStreamTransactionINS2_12IntrusivePtrINS1_12Leaderboards2V127RecordLargeDataResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common19UpStreamTransactionINS2_12IntrusivePtrINS1_12Leaderboards2V127RecordLargeDataResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common21DownStreamTransactionINS2_12IntrusivePtrINS1_12Leaderboards2V17ViewApi37GetLargeDataByObjectIdResponseHeadersEEEE5startEPNS2_10LibContextE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common21DownStreamTransactionINS2_12IntrusivePtrINS1_12Leaderboards2V17ViewApi37GetLargeDataByObjectIdResponseHeadersEEEE6finishEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common21DownStreamTransactionINS2_12IntrusivePtrINS1_12Leaderboards2V17ViewApi37GetLargeDataByObjectIdResponseHeadersEEEE8readDataEPcm() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common21DownStreamTransactionINS2_12IntrusivePtrINS1_12Leaderboards2V17ViewApi37GetLargeDataByObjectIdResponseHeadersEEEEC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common21DownStreamTransactionINS2_12IntrusivePtrINS1_12Leaderboards2V17ViewApi37GetLargeDataByObjectIdResponseHeadersEEEED1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common21DownStreamTransactionINS2_12IntrusivePtrINS1_17TitleCloudStorage2V17DataApi27DownloadDataResponseHeadersEEEE5startEPNS2_10LibContextE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common21DownStreamTransactionINS2_12IntrusivePtrINS1_17TitleCloudStorage2V17DataApi27DownloadDataResponseHeadersEEEE6finishEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common21DownStreamTransactionINS2_12IntrusivePtrINS1_17TitleCloudStorage2V17DataApi27DownloadDataResponseHeadersEEEE8readDataEPcm() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common21DownStreamTransactionINS2_12IntrusivePtrINS1_17TitleCloudStorage2V17DataApi27DownloadDataResponseHeadersEEEEC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common21DownStreamTransactionINS2_12IntrusivePtrINS1_17TitleCloudStorage2V17DataApi27DownloadDataResponseHeadersEEEED1Ev(void* self) { + (void)self; + return 0; +} + +struct CommonString { + std::int32_t referenceCount; + char* buffer; + std::uint64_t bufferSize; + void* libContext; +}; +static_assert(sizeof(CommonString) == 0x20 && offsetof(CommonString, buffer) == 0x08); +static_assert(offsetof(CommonString, bufferSize) == 0x10 && offsetof(CommonString, libContext) == 0x18); + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6StringC1EPNS2_10LibContextE(CommonString* self, void* libContext) { + self->referenceCount = 0; + self->buffer = nullptr; + self->bufferSize = 0; + self->libContext = libContext; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6StringD1Ev(CommonString* self) { + std::free(self->buffer); + self->buffer = nullptr; + self->bufferSize = 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_12Leaderboards2V14UserEEEE8pushBackERKS8_() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_12Leaderboards2V14UserEEEEC1EPNS2_10LibContextE(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_12Leaderboards2V14UserEEEED1Ev(void* self) { + (void)self; + return 0; +} + +void* APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_12Leaderboards2V15EntryEEEE3endEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +void* APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_12Leaderboards2V15EntryEEEE5beginEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +void* APS5_VABI _ZN3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_12Leaderboards2V15EntryEEEEppEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common9terminateERNS2_10LibContextE(uint64_t* context) { + *context = 0; + return 0; +} + +const void* APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V122GetRankingResponseBody22getLastUpdatedDateTimeEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V123RecordScoreResponseBody10getTmpRankEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +std::uint32_t APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V123RecordScoreResponseBody16getTmpSerialRankEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody11getSortModeEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +std::uint32_t APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody13getEntryLimitEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody13getUpdateModeEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +bool APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody13sortModeIsSetEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return false; +} + +bool APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody15entryLimitIsSetEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return false; +} + +bool APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody15updateModeIsSetEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return false; +} + +std::int64_t APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody16getMaxScoreLimitEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +std::int64_t APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody16getMinScoreLimitEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +bool APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody18maxScoreLimitIsSetEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return false; +} + +bool APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody18minScoreLimitIsSetEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return false; +} + +std::uint32_t APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody20getLargeDataNumLimitEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +std::size_t APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody21getLargeDataSizeLimitEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +bool APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody22largeDataNumLimitIsSetEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return false; +} + +bool APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody23largeDataSizeLimitIsSetEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return false; +} + +const char* APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry10getCommentEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +const char* APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry11getObjectIdEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +const char* APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry11getOnlineIdEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +bool APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry12commentIsSetEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return false; +} + +std::uint64_t APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry12getAccountIdEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +const void* APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry12getSmallDataEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +std::uint32_t APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry13getSerialRankEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +bool APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry13objectIdIsSetEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return false; +} + +std::uint32_t APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry14getHighestRankEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +bool APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry14smallDataIsSetEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return false; +} + +std::uint32_t APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry20getHighestSerialRankEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry7getPcIdEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +std::uint32_t APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry7getRankEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +std::int64_t APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry8getScoreEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +const char* APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V19RecordApi26RecordScoreResponseHeaders24getXPsnAtomicOperationIdEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE11getResponseERS8_() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEENS4_INS6_9RecordApi26RecordScoreResponseHeadersEEEE11getResponseERS8_() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE11getResponseERS8_() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V14UserEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V15EntryEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V19RecordApi26RecordScoreResponseHeadersEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6BinaryEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_12Leaderboards2V15EntryEEEEEEdeEv(const IntrusivePtr* self) { + return self->object; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEENS4_INS6_9RecordApi26RecordScoreResponseHeadersEEEE18getResponseHeadersERSB_() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +std::size_t APS5_VABI _ZNK3sce2Np9CppWebApi6Common6Binary4sizeEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +const void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common6Binary9getBinaryEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +const char* APS5_VABI _ZNK3sce2Np9CppWebApi6Common6String5c_strEv(const CommonString* self) { + return self->buffer != nullptr ? self->buffer : ""; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_12Leaderboards2V15EntryEEEEdeEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +bool APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_12Leaderboards2V15EntryEEEEneERKS9_(const void* self, const void* other) { + (void)self; + (void)other; + NotImplemented_nid_no_patch(__func__); + return false; +} + +// Shared vector bodies. A Vector handle is an IntrusivePtr whose object is a body; +// bodies adopted from a title through the pointer/deleter constructor keep their +// 32-bit reference count at 0x30, so bodies created here do the same. +static constexpr std::uint64_t VectorBodyMagic = 0x434e507056656342ULL; +struct VectorBody { + std::uint64_t magic; + std::uint8_t reserved[0x28]; + std::int32_t referenceCount; + std::int32_t elementKind; // 1 = IntrusivePtr elements, 2 = String elements + void* elements; + std::uint64_t size; + std::uint64_t capacity; +}; +static_assert(sizeof(VectorBody) == 0x50 && offsetof(VectorBody, referenceCount) == 0x30); + +static void APS5_VABI DestroyVectorBody(void* object) { + auto* body = static_cast(object); + if (body->elementKind == 1) { + auto* elements = static_cast(body->elements); + for (std::uint64_t index = 0; index < body->size; ++index) { + ReleaseIntrusivePtr(&elements[index], 0x00, __func__); + } + } else if (body->elementKind == 2) { + auto* elements = static_cast(body->elements); + for (std::uint64_t index = 0; index < body->size; ++index) { + std::free(elements[index].buffer); + } + } + std::free(body->elements); + std::free(body); +} + +static VectorBody* VectorBodyFor(IntrusivePtr* self, const std::int32_t elementKind, const char* function) { + auto* body = static_cast(self->object); + if (body == nullptr) { + body = static_cast(std::calloc(1, sizeof(VectorBody))); + if (body == nullptr) return nullptr; + body->magic = VectorBodyMagic; + body->referenceCount = 1; + body->elementKind = elementKind; + self->object = body; + self->deleter = &DestroyVectorBody; + return body; + } + if (body->magic != VectorBodyMagic || body->elementKind != elementKind) { + // appending to a title-provided body needs its element layout, which is unknown + NotImplemented_nid_no_patch(function); + return nullptr; + } + return body; +} + +static bool VectorGrow(VectorBody* body, const std::size_t elementSize) { + if (body->size < body->capacity) return true; + const std::uint64_t capacity = body->capacity != 0 ? body->capacity * 2 : 4; + void* elements = std::realloc(body->elements, static_cast(capacity) * elementSize); + if (elements == nullptr) return false; + body->elements = elements; + body->capacity = capacity; + return true; +} + +static int VectorPushBackIntrusivePtr(IntrusivePtr* self, const IntrusivePtr* element) { + VectorBody* body = VectorBodyFor(self, 1, __func__); + if (body == nullptr) return -1; + if (!VectorGrow(body, sizeof(IntrusivePtr))) return -1; + auto* elements = static_cast(body->elements); + elements[body->size] = *element; + if (element->object != nullptr) ReferenceCount(element->object, ReferenceCountOffset).fetch_add(1); + body->size += 1; + return 0; +} + +static int VectorPushBackString(IntrusivePtr* self, const CommonString* element) { + VectorBody* body = VectorBodyFor(self, 2, __func__); + if (body == nullptr) return -1; + if (!VectorGrow(body, sizeof(CommonString))) return -1; + CommonString copy = *element; + copy.referenceCount = 0; + copy.buffer = nullptr; + copy.bufferSize = 0; + if (element->buffer != nullptr) { + const std::size_t length = std::strlen(element->buffer) + 1; + copy.buffer = static_cast(std::malloc(length)); + if (copy.buffer == nullptr) return -1; + std::memcpy(copy.buffer, element->buffer, length); + copy.bufferSize = length; + } + static_cast(body->elements)[body->size] = copy; + body->size += 1; + return 0; +} +// -------------------------------------------------------------------------- +// SessionManager2V1 and extra Common symbols, from a title dump whose modules lack +// libSceNpCppWebApi.prx. +// -------------------------------------------------------------------------- + + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V115LocalizedStringEEC1ERS7_(IntrusivePtr* self, const IntrusivePtr* source) { + CopyIntrusivePtr(self, source, 0x00); +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V120RequestPlayerSessionEEptEv(const IntrusivePtr* self) { + return self->object; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V120RequestPlayerSession16setMaxSpectatorsERKi() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V139RequestPlayerSessionMemberPlayerFactory6createEPNS1_6Common10LibContextERKNS5_6VectorINS5_12IntrusivePtrINS3_32RequestCreatePlayerSessionPlayerEEEEEPNS9_INS3_32RequestPlayerSessionMemberPlayerEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +void* APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V136UsersPlayerSessionsInvitationForReadEEEE3endEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToJoinPlayerSessionAsPlayerD1Ev() { + return 0; +} + +const char* APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V110FromMember11getOnlineIdEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi38ParameterToGetPlayerSessionInvitations26setinvitationInvalidFilterEPKc() { + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V120RequestPlayerSessionEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_14SessionManager2V152PostPlayerSessionsSessionIdMemberPlayersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE11getResponseERS8_() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V126RequestPlayerSessionPlayerEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V151PostPlayerSessionsSessionIdMemberPlayersRequestBodyEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi29ParameterToLeavePlayerSessionD1Ev() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V122LocalizedStringFactory6createEPNS1_6Common10LibContextEPKcNS_4Json6ObjectEPNS5_12IntrusivePtrINS3_15LocalizedStringEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V151PostPlayerSessionsSessionIdMemberPlayersRequestBodyEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x00, __func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V132RequestCreatePlayerSessionPlayerEEEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x30, __func__); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common13ConstIteratorINS2_12IntrusivePtrINS1_14SessionManager2V136UsersPlayerSessionsInvitationForReadEEEED2Ev(void* self) { + (void)self; + return 0; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V113PlayerSessionEEptEv(const IntrusivePtr* self) { + return self->object; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V120RequestPlayerSession20setInvitableUserTypeERKNS3_17InvitableUserTypeE() { + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V132RequestCreatePlayerSessionPlayerEEEEC1EPNS2_10LibContextE(IntrusivePtr* self, void* libContext) { + self->object = nullptr; + self->deleter = nullptr; + self->libContext = libContext; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V132RequestCreatePlayerSessionPlayerEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x00, __func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEEC1EPNS2_10LibContextE(IntrusivePtr* self, void* libContext) { + self->object = nullptr; + self->deleter = nullptr; + self->libContext = libContext; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V120RequestPlayerSessionEEEE8pushBackERKS8_(IntrusivePtr* self, const void* element) { + return VectorPushBackIntrusivePtr(self, static_cast(element)); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V152PostPlayerSessionsSessionIdMemberPlayersResponseBodyEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x00, __func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x30, __func__); +} + +void* APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V136UsersPlayerSessionsInvitationForReadEEEE5beginEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V129PostPlayerSessionsRequestBodyEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_14SessionManager2V136UsersPlayerSessionsInvitationForReadEEEEEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x30, __func__); +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_14SessionManager2V136UsersPlayerSessionsInvitationForReadEEEEEEdeEv(const IntrusivePtr* self) { + return self->object; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_15DefaultResponseENS2_12IntrusivePtrINS2_18ResponseHeaderBaseEEEEC1Ev(void* self) { + (void)self; + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V126RequestPlayerSessionPlayerEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x00, __func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V115LocalizedStringEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_14SessionManager2V154GetUsersAccountIdPlayerSessionsInvitationsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void* self) { + (void)self; + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V151PostPlayerSessionsSessionIdMemberPlayersRequestBodyEEC1ERS7_(IntrusivePtr* self, const IntrusivePtr* source) { + CopyIntrusivePtr(self, source, 0x00); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi25joinPlayerSessionAsPlayerEiRKNS4_36ParameterToJoinPlayerSessionAsPlayerERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_52PostPlayerSessionsSessionIdMemberPlayersResponseBodyEEENSA_INS8_18ResponseHeaderBaseEEEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6String6appendEPKc(CommonString* self, const char* text) { + if (text == nullptr) return 0; + const std::size_t textLength = std::strlen(text); + const std::size_t oldLength = self->buffer != nullptr ? std::strlen(self->buffer) : 0; + const std::size_t needed = oldLength + textLength + 1; + if (needed > self->bufferSize) { + std::size_t capacity = self->bufferSize != 0 ? static_cast(self->bufferSize) : 16; + while (capacity < needed) capacity *= 2; + char* buffer = static_cast(std::malloc(capacity)); + if (buffer == nullptr) return -1; + if (oldLength != 0) std::memcpy(buffer, self->buffer, oldLength); + std::free(self->buffer); + self->buffer = buffer; + self->bufferSize = capacity; + } + std::memcpy(self->buffer + oldLength, text, textLength + 1); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V132RequestCreatePlayerSessionPlayerEEEE8pushBackERKS8_(IntrusivePtr* self, const void* element) { + return VectorPushBackIntrusivePtr(self, static_cast(element)); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15DefaultResponseC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToJoinPlayerSessionAsPlayer9terminateEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi31ParameterToCreatePlayerSessions10initializeEPNS1_6Common10LibContextENS6_12IntrusivePtrINS3_29PostPlayerSessionsRequestBodyEEE() { + return 0; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V110FromMemberEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V132RequestPlayerSessionMemberPlayerEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x00, __func__); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_14SessionManager2V130PostPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void* self) { + (void)self; + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V126RequestPlayerSessionPlayerEEEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x30, __func__); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi29ParameterToLeavePlayerSessionC1Ev() { + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V130PostPlayerSessionsResponseBodyEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x00, __func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V126RequestPlayerSessionPlayerEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +const char* APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V136UsersPlayerSessionsInvitationForRead12getSessionIdEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V136UsersPlayerSessionsInvitationForRead7getFromEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V129PostPlayerSessionsRequestBodyEEC1ERS7_(IntrusivePtr* self, const IntrusivePtr* source) { + CopyIntrusivePtr(self, source, 0x00); +} + +void* APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V130PostPlayerSessionsResponseBody17getPlayerSessionsEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi18leavePlayerSessionEiRKNS4_29ParameterToLeavePlayerSessionERNS1_6Common11TransactionINS8_15DefaultResponseENS8_12IntrusivePtrINS8_18ResponseHeaderBaseEEEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V124PlayerSessionPushContextEEEE8pushBackERKS8_(IntrusivePtr* self, const void* element) { + return VectorPushBackIntrusivePtr(self, static_cast(element)); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V152PostPlayerSessionsSessionIdMemberPlayersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi31ParameterToCreatePlayerSessionsD1Ev() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi27getPlayerSessionInvitationsEiRKNS4_38ParameterToGetPlayerSessionInvitationsERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_54GetUsersAccountIdPlayerSessionsInvitationsResponseBodyEEENSA_INS8_18ResponseHeaderBaseEEEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V132RequestPlayerSessionMemberPlayerEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V120RequestPlayerSessionEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x00, __func__); +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_14SessionManager2V113PlayerSessionEEEEEEdeEv(const IntrusivePtr* self) { + return self->object; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V124PlayerSessionPushContextEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V130PostPlayerSessionsResponseBodyEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V110FromMemberEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x00, __func__); +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V154GetUsersAccountIdPlayerSessionsInvitationsResponseBodyEEptEv(const IntrusivePtr* self) { + return self->object; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi38ParameterToGetPlayerSessionInvitationsC1Ev() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V126RequestPlayerSessionPlayerEEEE8pushBackERKS8_(IntrusivePtr* self, const void* element) { + return VectorPushBackIntrusivePtr(self, static_cast(element)); +} + +void* APS5_VABI _ZN3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_14SessionManager2V136UsersPlayerSessionsInvitationForReadEEEEppEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_14SessionManager2V152PostPlayerSessionsSessionIdMemberPlayersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V120RequestPlayerSession19setJoinableUserTypeERKNS3_16JoinableUserTypeE() { + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_14SessionManager2V154GetUsersAccountIdPlayerSessionsInvitationsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE11getResponseERS8_() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi38ParameterToGetPlayerSessionInvitationsD1Ev() { + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_14SessionManager2V113PlayerSessionEEEEEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x30, __func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V132RequestPlayerSessionMemberPlayerEEC1ERS7_(IntrusivePtr* self, const IntrusivePtr* source) { + CopyIntrusivePtr(self, source, 0x00); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V130PostPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { + return 0; +} + +bool APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V136UsersPlayerSessionsInvitationForRead14sessionIdIsSetEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return false; +} + +bool APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V136UsersPlayerSessionsInvitationForRead17invitationIdIsSetEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return false; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V154GetUsersAccountIdPlayerSessionsInvitationsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi31ParameterToCreatePlayerSessionsC1Ev() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToJoinPlayerSessionAsPlayer10initializeEPNS1_6Common10LibContextEPKcNS6_12IntrusivePtrINS3_51PostPlayerSessionsSessionIdMemberPlayersRequestBodyEEE() { + return 0; +} + +bool APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V154GetUsersAccountIdPlayerSessionsInvitationsResponseBody16invitationsIsSetEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return false; +} + +void* APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V113PlayerSessionEEEEixEm(void* self, std::uint64_t index) { + (void)self; + (void)index; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi29ParameterToLeavePlayerSession10initializeEPNS1_6Common10LibContextEPKcSA_() { + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V115LocalizedStringEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x00, __func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS2_6StringEEEEC1ERS7_(IntrusivePtr* self, const IntrusivePtr* source) { + CopyIntrusivePtr(self, source, 0x30); +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_14SessionManager2V136UsersPlayerSessionsInvitationForReadEEEEdeEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi38ParameterToGetPlayerSessionInvitations9setfieldsENS1_6Common12IntrusivePtrINS6_6VectorINS6_6StringEEEEE() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15DefaultResponseD1Ev(void* self) { + (void)self; + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS2_6StringEEEEC1EPS6_PFvS8_EPNS2_10LibContextE(IntrusivePtr* self, void* body, void (APS5_VABI* deleter)(void*), void* libContext) { + self->object = body; + self->deleter = deleter; + self->libContext = libContext; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V120RequestPlayerSessionEEEEC1EPNS2_10LibContextE(IntrusivePtr* self, void* libContext) { + self->object = nullptr; + self->deleter = nullptr; + self->libContext = libContext; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V133RequestPlayerSessionPlayerFactory6createEPNS1_6Common10LibContextEPKcS9_PNS5_12IntrusivePtrINS3_26RequestPlayerSessionPlayerEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi20createPlayerSessionsEiRKNS4_31ParameterToCreatePlayerSessionsERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_30PostPlayerSessionsResponseBodyEEENSA_INS8_18ResponseHeaderBaseEEEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi36ParameterToJoinPlayerSessionAsPlayerC1Ev() { + return 0; +} + +bool APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V136UsersPlayerSessionsInvitationForRead9fromIsSetEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return false; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V126RequestPlayerSessionPlayer15setPushContextsERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_24PlayerSessionPushContextEEEEE() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V127RequestPlayerSessionFactory6createEPNS1_6Common10LibContextEiNS5_12IntrusivePtrINS3_32RequestPlayerSessionMemberPlayerEEERKNS5_6VectorINS5_6StringEEENS8_INS3_15LocalizedStringEEEPNS8_INS3_20RequestPlayerSessionEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_14SessionManager2V130PostPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE11getResponseERS8_() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V126RequestPlayerSessionPlayerEEEEC1EPNS2_10LibContextE(IntrusivePtr* self, void* libContext) { + self->object = nullptr; + self->deleter = nullptr; + self->libContext = libContext; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V124PlayerSessionPushContextEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x00, __func__); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V131PlayerSessionPushContextFactory6createEPNS1_6Common10LibContextEPKcPNS5_12IntrusivePtrINS3_24PlayerSessionPushContextEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V129PostPlayerSessionsRequestBodyEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x00, __func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V154GetUsersAccountIdPlayerSessionsInvitationsResponseBodyEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEE8pushBackERKS4_(IntrusivePtr* self, const void* element) { + return VectorPushBackString(self, static_cast(element)); +} + +bool APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_14SessionManager2V136UsersPlayerSessionsInvitationForReadEEEEneERKS9_(const void* self, const void* other) { + (void)self; + (void)other; + NotImplemented_nid_no_patch(__func__); + return false; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V117PlayerSessionsApi38ParameterToGetPlayerSessionInvitations10initializeEPNS1_6Common10LibContextEPKc() { + return 0; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V130PostPlayerSessionsResponseBodyEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V154GetUsersAccountIdPlayerSessionsInvitationsResponseBodyEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x00, __func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS2_6StringEEEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x30, __func__); +} + +void* APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V154GetUsersAccountIdPlayerSessionsInvitationsResponseBody14getInvitationsEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V132RequestCreatePlayerSessionPlayerEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V152PostPlayerSessionsSessionIdMemberPlayersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_15DefaultResponseENS2_12IntrusivePtrINS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_15DefaultResponseENS2_12IntrusivePtrINS2_18ResponseHeaderBaseEEEE6finishEv() { + return 0; +} + +bool APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V136UsersPlayerSessionsInvitationForRead22receivedTimestampIsSetEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return false; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_14SessionManager2V130PostPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void* self) { + (void)self; + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V120RequestPlayerSessionEEEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x30, __func__); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V154GetUsersAccountIdPlayerSessionsInvitationsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_15DefaultResponseENS2_12IntrusivePtrINS2_18ResponseHeaderBaseEEEE11getResponseERS4_() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_14SessionManager2V154GetUsersAccountIdPlayerSessionsInvitationsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V139RequestCreatePlayerSessionPlayerFactory6createEPNS1_6Common10LibContextEPKcS9_RKNS5_6VectorINS5_12IntrusivePtrINS3_24PlayerSessionPushContextEEEEEPNSB_INS3_32RequestCreatePlayerSessionPlayerEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_14SessionManager2V152PostPlayerSessionsSessionIdMemberPlayersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void* self) { + (void)self; + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V124PlayerSessionPushContextEEEEC1EPNS2_10LibContextE(IntrusivePtr* self, void* libContext) { + self->object = nullptr; + self->deleter = nullptr; + self->libContext = libContext; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V124PlayerSessionPushContextEEEED1Ev(IntrusivePtr* self) { + ReleaseIntrusivePtr(self, 0x30, __func__); +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V136UsersPlayerSessionsInvitationForReadEEptEv(const IntrusivePtr* self) { + return self->object; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V152PostPlayerSessionsSessionIdMemberPlayersResponseBodyEEC1Ev(IntrusivePtr* self) { + *self = {}; +} + +const char* APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V113PlayerSession12getSessionIdEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V158PostPlayerSessionsSessionIdMemberPlayersRequestBodyFactory6createEPNS1_6Common10LibContextERKNS5_6VectorINS5_12IntrusivePtrINS3_26RequestPlayerSessionPlayerEEEEEPNS9_INS3_51PostPlayerSessionsSessionIdMemberPlayersRequestBodyEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V136PostPlayerSessionsRequestBodyFactory6createEPNS1_6Common10LibContextERKNS5_6VectorINS5_12IntrusivePtrINS3_20RequestPlayerSessionEEEEEPNS9_INS3_29PostPlayerSessionsRequestBodyEEE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V130PostPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE() { + return SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi14SessionManager2V120RequestPlayerSession28setExclusiveLeaderPrivilegesERKNS1_6Common6VectorINS5_6StringEEE() { + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_15DefaultResponseENS2_12IntrusivePtrINS2_18ResponseHeaderBaseEEEED1Ev(void* self) { + (void)self; + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common13ConstIteratorINS2_12IntrusivePtrINS1_13InGameCatalog2V57ProductEEEED2Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common13ConstIteratorINS2_12IntrusivePtrINS1_17TitleCloudStorage2V110DataStatusEEEED2Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_13InGameCatalog2V59ContainerEEEEEEC1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_13InGameCatalog2V59ContainerEEEEEEdeEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_13InGameCatalog2V59ContainerEEEEEEptEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_13InGameCatalog2V59ContainerEEEEEED1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_13InGameCatalog2V57ProductEEEEEEptEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_13InGameCatalog2V57ProductEEEEEED1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_13InGameCatalog2V53SkuEEEEEEptEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_13InGameCatalog2V53SkuEEEEEED1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_17TitleCloudStorage2V110DataStatusEEEEEEdeEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_17TitleCloudStorage2V110DataStatusEEEEEED1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_13InGameCatalog2V59ContainerEEC1ERS7_(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_13InGameCatalog2V59ContainerEEptEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_13InGameCatalog2V59ContainerEED1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_13InGameCatalog2V57ProductEEC1ERS7_(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_13InGameCatalog2V57ProductEEptEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_13InGameCatalog2V57ProductEED1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_17TitleCloudStorage2V110DataStatusEEptEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_17TitleCloudStorage2V132GetMultiDataStatusesResponseBodyEEC1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_17TitleCloudStorage2V132GetMultiDataStatusesResponseBodyEEptEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_17TitleCloudStorage2V132GetMultiDataStatusesResponseBodyEED1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_17TitleCloudStorage2V15OwnerEEptEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_17TitleCloudStorage2V15OwnerEED1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +bool APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_13InGameCatalog2V57ProductEEEEneERKS9_(const void* self, const void* other) { + (void)self; + (void)other; + NotImplemented_nid_no_patch(__func__); + return false; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_13InGameCatalog2V57ProductEEEEdeEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +void* APS5_VABI _ZN3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_13InGameCatalog2V57ProductEEEEppEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +bool APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_17TitleCloudStorage2V110DataStatusEEEEneERKS9_(const void* self, const void* other) { + (void)self; + (void)other; + NotImplemented_nid_no_patch(__func__); + return false; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_17TitleCloudStorage2V110DataStatusEEEEdeEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +void* APS5_VABI _ZN3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_17TitleCloudStorage2V110DataStatusEEEEppEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS2_6VectorINS4_INS1_13InGameCatalog2V59ContainerEEEEEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS2_6VectorINS4_INS1_13InGameCatalog2V59ContainerEEEEEEENS4_INS2_18ResponseHeaderBaseEEEE11getResponseERSB_(const void* self, void* response) { + (void)self; + (void)response; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS2_6VectorINS4_INS1_13InGameCatalog2V59ContainerEEEEEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_17TitleCloudStorage2V132GetMultiDataStatusesResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_17TitleCloudStorage2V132GetMultiDataStatusesResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE11getResponseERS8_(const void* self, void* response) { + (void)self; + (void)response; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_17TitleCloudStorage2V132GetMultiDataStatusesResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS2_6VectorINS4_INS1_13InGameCatalog2V59ContainerEEEEEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS2_6VectorINS4_INS1_13InGameCatalog2V59ContainerEEEEEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE(void* self, void* context) { + (void)self; + (void)context; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_17TitleCloudStorage2V132GetMultiDataStatusesResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_17TitleCloudStorage2V132GetMultiDataStatusesResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE(void* self, void* context) { + (void)self; + (void)context; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +void* APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_13InGameCatalog2V59ContainerEEEEixEm(void* self, std::uint64_t index) { + (void)self; + (void)index; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +std::size_t APS5_VABI _ZNK3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_13InGameCatalog2V59ContainerEEEE4sizeEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +void* APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_13InGameCatalog2V57ProductEEEE5beginEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +void* APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_13InGameCatalog2V57ProductEEEE3endEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +std::size_t APS5_VABI _ZNK3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_13InGameCatalog2V53SkuEEEE4sizeEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +void* APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_17TitleCloudStorage2V110DataStatusEEEE5beginEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +void* APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_17TitleCloudStorage2V110DataStatusEEEE3endEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +void* APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V59Container11getChildrenEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainerC1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer10initializeEPNS1_6Common10LibContextE(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +bool APS5_VABI _ZNK3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer13isInitializedEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return false; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer15setserviceLabelEi(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainer9terminateEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi23ParameterToGetContainerD1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V512ContainerApi12getContainerEiRKNS4_23ParameterToGetContainerERNS1_6Common11TransactionINS8_12IntrusivePtrINS8_6VectorINSA_INS3_9ContainerEEEEEEENSA_INS8_18ResponseHeaderBaseEEEEE(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +const char* APS5_VABI _ZNK3sce2Np9CppWebApi13InGameCatalog2V57Product8getLabelEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +void* APS5_VABI _ZN3sce2Np9CppWebApi13InGameCatalog2V57Product7getSkusEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi13InGameCatalog2V57Product7getTypeEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +bool APS5_VABI _ZNK3sce2Np9CppWebApi13InGameCatalog2V57Product9skusIsSetEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return false; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi23ParameterToDownloadData11setobjectIdEPKc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi37ParameterToGetMultiDataStatusesByUserC1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi37ParameterToGetMultiDataStatusesByUser10initializeEPNS1_6Common10LibContextEPKcSA_(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi37ParameterToGetMultiDataStatusesByUser9setfieldsEPKc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi37ParameterToGetMultiDataStatusesByUser9terminateEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi37ParameterToGetMultiDataStatusesByUserD1Ev(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi26getMultiDataStatusesByUserEiRKNS4_37ParameterToGetMultiDataStatusesByUserERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_32GetMultiDataStatusesResponseBodyEEENSA_INS8_18ResponseHeaderBaseEEEEE(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +std::size_t APS5_VABI _ZNK3sce2Np9CppWebApi17TitleCloudStorage2V110DataStatus11getDataSizeEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +const char* APS5_VABI _ZNK3sce2Np9CppWebApi17TitleCloudStorage2V110DataStatus11getObjectIdEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +void* APS5_VABI _ZNK3sce2Np9CppWebApi17TitleCloudStorage2V110DataStatus8getOwnerEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +int APS5_VABI _ZNK3sce2Np9CppWebApi17TitleCloudStorage2V110DataStatus9getSlotIdEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +void* APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V132GetMultiDataStatusesResponseBody17getDataStatusListEv(void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + +std::uint32_t APS5_VABI _ZNK3sce2Np9CppWebApi17TitleCloudStorage2V132GetMultiDataStatusesResponseBody23getTotalDataStatusCountEv(const void* self) { + (void)self; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +std::uint64_t APS5_VABI _ZNK3sce2Np9CppWebApi17TitleCloudStorage2V15Owner12getAccountIdEv(const void* self) { + (void)self; + 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..c8be4b9f09 100644 --- a/core/libs/prx/libSceNpGameIntent/Export.cpp +++ b/core/libs/prx/libSceNpGameIntent/Export.cpp @@ -12,6 +12,7 @@ constexpr int NpGameIntentErrorIntentNotFound = -2141898746; constexpr int NpGameIntentErrorValueNotFound = -2141898745; constexpr int NpGameIntentUserIdInvalid = -1; + } extern "C" { @@ -73,4 +74,53 @@ int APS5_VABI sceNpGameIntentVshTerminate(void) { return 0; } +int APS5_VABI sceNpAppLauncherInitialize(const void* initParam) { + (void)initParam; + return 0; +} + +int APS5_VABI sceNpAppLauncherLaunchApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpAppLauncherTerminate() { + 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/libSceNpManager/Export.cpp b/core/libs/prx/libSceNpManager/Export.cpp index 17237a6caf..f1f3403abb 100644 --- a/core/libs/prx/libSceNpManager/Export.cpp +++ b/core/libs/prx/libSceNpManager/Export.cpp @@ -60,6 +60,607 @@ StateCallback stateCallback; extern "C" { +int APS5_VABI sceNpAsmAbort(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientAbortRequest(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientClearNpTitleToken(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientClearNpTitleTokenA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientCreateRequest2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientCreateResourceContext(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientCreateResourceContext2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientDeleteRequest(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientDeleteResourceContext(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientDeleteResourceContext2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientGetAppId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientGetCacheControlMaxAge(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientGetGameNpTitleInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientGetGameNpTitleToken(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientGetGameTitleBanInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientGetNpComInfo2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientGetNpComInfo2A(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientGetNpComInfo3(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientGetNpComInfo4(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientGetNpTitleId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientGetNpTitleToken(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientGetNpTitleToken2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientGetNpTitleTokenA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientGetRelatedGameNpTitleIds(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientGetRelatedGameNpTitleIdsA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientGetRelatedGameNpTitleIdsResult(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientGetServiceBaseUrl(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientGetServiceBaseUrlA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientGetServiceBaseUrlWithNpTitleId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientGetServiceBaseUrlWithNpTitleIdA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientGetServiceIdInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientGetServiceIdInfoA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientInitialize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientSetNpTitleId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmClientTerminate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmCreateConnection(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmCreateRequest(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmDeleteConnection(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmDeleteRequest(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmGenerateNpTitleToken(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmGenerateNpTitleToken2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmGetNpCommInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmGetNpCommInfo2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmGetRelatedGameNpTitleIds(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmGetServiceBaseUrl(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmGetServiceIdInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmInitialize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpAsmTerminate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpCheckNpAvailabilityA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpGetAccountDateOfBirthA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpGetAccountLanguageA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpGetGamePresenceStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpGetGamePresenceStatusA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpGetParentalControlInfoA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpIdMapperAbortRequest(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpIdMapperAccountIdToNpId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpIdMapperAccountIdToOnlineId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpIdMapperCreateRequest(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpIdMapperDeleteRequest(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpIdMapperNpIdToAccountId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpIdMapperOnlineIdToAccountId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpInGameMessageAbortHandle(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpInGameMessageCheckCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpInGameMessageGetMemoryPoolStatistics(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpInGameMessagePrepareA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpInGameMessageSendDataA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpIntGetAppType(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpIntGetGamePresenceStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpIntGetNpTitleId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpIntGetNpTitleIdSecret(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpIntRegisterCheckCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpIntRegisterGamePresenceCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpIsPlusMember(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntAbortRequest(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntAddActiveSigninStateCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntAddOnlineIdChangeCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntAddPlusMemberTypeCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntAddSigninStateCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntAuthGetAuthorizationCode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntAuthGetIdToken(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntCheckNpAvailability(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntCheckNpAvailabilityByPid(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntCheckNpState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntCheckNpStateA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntClearGameAccessToken(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntClearOnlineIdChangeFlag(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntClearTicket(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntClearUsedFlag(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntClearVshAccessToken(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntCreateAuthenticationTicket(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntCreateLoginContext(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntCreateLoginRequest(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntCreateRequest(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntDeleteLoginContext(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntDeleteRequest(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetAccessTokenViaImplicitFlow(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetAccountAge(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetAccountCountry(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetAccountCountryA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetAccountCountrySdk(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetAccountDateOfBirthSdk(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetAccountId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetAccountIdSdk(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetAccountLanguage(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetAccountLanguageA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetAccountNpEnv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetAccountType(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetActiveSigninState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetAuthServerErrorFlag(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetAuthorizationCode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetAuthorizationCodeWithPsnoUri(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetClientCredentialAccessToken(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetCommunicationRestrictionStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetGameAccessToken(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetIssuerId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetNpEnv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetNpId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetNpIdByOnlineId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetNpIdSdk(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetOfflineAccountId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetOnlineId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetOnlineIdChangeFlag(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetOnlineIdSdk(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetParentalControlFlag(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetParentalControlInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetPlusMemberType(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetPlusMemberTypeNB(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetServerError(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetSigninState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetSigninTelemetryInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetTicket(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetTicketA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetTitleToken(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetTitleTokenWithCheck(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetUserIdByAccountId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetUserIdByNpId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetUserIdByOnlineId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetUserIdByOnlineIdSdk(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetUserList(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetUserNum(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetVshAccessToken(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetVshAccessTokenWithCheck(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntGetVshClientId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntIsSubAccount(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntIsTemporarySignout(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntIsUnregisteredClientError(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntLoginAuthenticate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntLoginBind(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntLoginCreateAuthenticationTicket(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +int APS5_VABI sceNpManagerIntLoginGetAccessToken(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceNpAbortRequest(int req_id) { (void)req_id; return 0; @@ -125,7 +726,7 @@ int APS5_VABI sceNpGetAccountAge(int req_id, int user_id, uint8_t* age) { int APS5_VABI sceNpGetAccountCountryA(int user_id, void* country_code) { (void)user_id; - (void)country_code; + if (!country_code) return SCE_NP_ERROR_INVALID_ARGUMENT; return SCE_NP_ERROR_SIGNED_OUT; } @@ -150,7 +751,7 @@ int APS5_VABI sceNpGetNpReachabilityState(int user_id, uint32_t* state) { int APS5_VABI sceNpGetOnlineId(int user_id, NpOnlineId* online_id) { (void)user_id; - (void)online_id; + if (!online_id) return SCE_NP_ERROR_INVALID_ARGUMENT; return SCE_NP_ERROR_SIGNED_OUT; } @@ -175,9 +776,8 @@ int APS5_VABI sceNpPollAsync(int req_id, int* result) { } void APS5_VABI sceNpRegisterGamePresenceCallback(void* callback, void* userdata) { - (void)callback; - (void)userdata; - NotImplemented_nid_no_patch(__func__); + (void)callback; + (void)userdata; } int APS5_VABI sceNpRegisterNpReachabilityStateCallback(void* callback, void* userdata) { @@ -317,4 +917,10 @@ int APS5_VABI sceNpUnknown_WxLcN76q19o(void) { return 0; } + +int APS5_VABI sceNpCheckCallbackForLib() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceNpParty/Export.cpp b/core/libs/prx/libSceNpParty/Export.cpp index 8440dbddcf..67fdadc679 100644 --- a/core/libs/prx/libSceNpParty/Export.cpp +++ b/core/libs/prx/libSceNpParty/Export.cpp @@ -3,9 +3,141 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +constexpr int NP_PARTY_ERROR_INVALID_ARGUMENT = static_cast(0x80552504u); +constexpr std::uint16_t NP_PARTY_STATE_NOT_IN_PARTY = 2; + +} + extern "C" { -int APS5_VABI sceNpPartyGetState(void* state) { +int APS5_VABI sceNpPartyGetState(std::uint16_t* state) { + if (!state) return NP_PARTY_ERROR_INVALID_ARGUMENT; + *state = NP_PARTY_STATE_NOT_IN_PARTY; + return 0; +} +int APS5_VABI sceNpPartyCheckCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPartyCreate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPartyCreateA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPartyGetId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPartyGetMemberInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPartyGetMemberInfoA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPartyGetMemberSessionInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPartyGetMemberVoiceInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPartyGetMembers(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPartyGetMembersA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPartyGetStateAsUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPartyGetStateAsUserA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPartyGetVoiceChatPriority(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPartyInitialize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPartyJoin(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPartyLeave(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPartyRegisterHandler(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPartyRegisterHandlerA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPartyRegisterPrivateHandler(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPartySendBinaryMessage(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPartySetVoiceChatPriority(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPartyShowInvitationList(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPartyShowInvitationListA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPartyTerminate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpPartyUnregisterPrivateHandler(void) { 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/libSceNpSnsFacebookDialog/Export.cpp b/core/libs/prx/libSceNpSnsFacebookDialog/Export.cpp new file mode 100644 index 0000000000..58bb27df19 --- /dev/null +++ b/core/libs/prx/libSceNpSnsFacebookDialog/Export.cpp @@ -0,0 +1,18 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceNpSnsFacebookDialogClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsFacebookDialogGetStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceNpTrophy2/Export.cpp b/core/libs/prx/libSceNpTrophy2/Export.cpp new file mode 100644 index 0000000000..9befeb73c9 --- /dev/null +++ b/core/libs/prx/libSceNpTrophy2/Export.cpp @@ -0,0 +1,123 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceNpTrophy2SystemAbortHandle(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophy2SystemBuildGroupIconUri(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophy2SystemBuildRewardIconUri(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophy2SystemBuildTrophyIconUri(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophy2SystemCheckCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophy2SystemCheckNetSyncTitles(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophy2SystemCreateContext(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophy2SystemCreateHandle(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophy2SystemDebugLockTrophy(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophy2SystemDebugUnlockTrophy(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophy2SystemDestroyContext(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophy2SystemGetTrophyDetails(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophy2SystemGetTrophyGroupConf(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophy2SystemGetTrophySetArray(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophy2SystemGetTrophyTitleConf(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophy2SystemGetTrophyTitleDetails(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophy2SystemGetTrpIconByUri(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophy2SystemIsServerAvailable(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophy2SystemRegisterTitleSyncedCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophy2SystemRegisterTitleUpdateCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophy2SystemRemoveAll(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophy2SystemRemoveTitleData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophy2SystemRemoveUserData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceNpTus/Export.cpp b/core/libs/prx/libSceNpTus/Export.cpp index 11b8a99218..a69e6cb5a6 100644 --- a/core/libs/prx/libSceNpTus/Export.cpp +++ b/core/libs/prx/libSceNpTus/Export.cpp @@ -20,6 +20,7 @@ static constexpr int MAX_SLOTS = 64; namespace { struct Request { + int titleCtxId = 0; bool started = false; bool aborted = false; int result = 0; @@ -41,6 +42,15 @@ int ValidateSlots(const std::int32_t* slotIds, int arrayLen, const void* option) return 0; } +int ValidateNonEmptySlots(const std::int32_t* slotIds, int arrayLen, const void* option) { + if (!slotIds || arrayLen < 1) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + return ValidateSlots(slotIds, arrayLen, option); +} + +bool IsEmpty(const NpOnlineId* user) { + return !user->data[0]; +} + int StartOffline(int reqId) { std::lock_guard lock(tusMutex); const auto request = requests.find(reqId); @@ -49,6 +59,22 @@ int StartOffline(int reqId) { request->second.result = request->second.aborted ? SCE_NP_COMMUNITY_ERROR_ABORTED : SCE_NP_ERROR_SIGNED_OUT; return 0; } + +int WaitOffline(int reqId, int* result) { + std::lock_guard lock(tusMutex); + const auto request = requests.find(reqId); + if (request == requests.end()) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; + if (!request->second.started) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (result) *result = request->second.result; + return 0; +} + +int Complete(int reqId, int asyncResult) { + if (asyncResult < 0) return asyncResult; + int result = 0; + if (const int error = WaitOffline(reqId, &result)) return error; + return result; +} } extern "C" { @@ -68,10 +94,17 @@ int APS5_VABI sceNpTusCreateRequest(int titleCtxId) { if (!titleContexts.contains(titleCtxId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; if (requests.size() >= MAX_REQUESTS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_OBJECTS; const int reqId = nextRequest++; - requests.emplace(reqId, Request{}); + requests.emplace(reqId, Request{titleCtxId}); return reqId; } +int APS5_VABI sceNpTusDeleteNpTitleCtx(int titleCtxId) { + std::lock_guard lock(tusMutex); + if (!titleContexts.erase(titleCtxId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; + std::erase_if(requests, [titleCtxId](const auto& entry) { return entry.second.titleCtxId == titleCtxId; }); + return 0; +} + int APS5_VABI sceNpTusDeleteRequest(int reqId) { std::lock_guard lock(tusMutex); return requests.erase(reqId) ? 0 : SCE_NP_COMMUNITY_ERROR_INVALID_ID; @@ -126,4 +159,627 @@ int APS5_VABI sceNpTusAddAndGetVariableAAsync(int reqId, std::uint64_t targetAcc return StartOffline(reqId); } +int APS5_VABI sceNpTusSetThreadParam(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTssCreateNpTitleCtx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTssCreateNpTitleCtxA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusCreateTitleCtx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusSetTimeout(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusChangeModeForOtherSaveDataOwners(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusWaitAsync(int reqId, int* result) { + return WaitOffline(reqId, result); +} + +int APS5_VABI sceNpTusGetMultiSlotVariableA(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiSlotVariableAAsync(reqId, targetAccountId, slotIds, variableArray, variableArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiSlotVariableAsync(int reqId, const void* targetNpId, const std::int32_t* slotIds, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + (void)targetNpId; + (void)variableArraySize; + if (!variableArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (const int error = ValidateSlots(slotIds, arrayLen, option)) return error; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiSlotVariableVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + (void)variableArraySize; + if (!targetVirtualUserId || !variableArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (const int error = ValidateNonEmptySlots(slotIds, arrayLen, option)) return error; + if (IsEmpty(targetVirtualUserId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiSlotVariableAVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + (void)variableArraySize; + if (!targetVirtualUserId || !variableArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (const int error = ValidateSlots(slotIds, arrayLen, option)) return error; + if (IsEmpty(targetVirtualUserId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiSlotVariableAVUser(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiSlotVariableAVUserAsync(reqId, targetVirtualUserId, slotIds, variableArray, variableArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiSlotVariableForCrossSaveAsync(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + (void)targetAccountId; + (void)variableArraySize; + if (!variableArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (const int error = ValidateSlots(slotIds, arrayLen, option)) return error; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiSlotVariableForCrossSave(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiSlotVariableForCrossSaveAsync(reqId, targetAccountId, slotIds, variableArray, variableArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiSlotVariableForCrossSaveVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + (void)variableArraySize; + if (!targetVirtualUserId || !variableArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (const int error = ValidateSlots(slotIds, arrayLen, option)) return error; + if (IsEmpty(targetVirtualUserId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusGetMultiSlotVariableForCrossSaveVUser(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option) { + return Complete(reqId, sceNpTusGetMultiSlotVariableForCrossSaveVUserAsync(reqId, targetVirtualUserId, slotIds, variableArray, variableArraySize, arrayLen, option)); +} + +int APS5_VABI sceNpTusSetMultiSlotVariableA(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, const std::int64_t* variableArray, int arrayLen, void* option) { + return Complete(reqId, sceNpTusSetMultiSlotVariableAAsync(reqId, targetAccountId, slotIds, variableArray, arrayLen, option)); +} + +int APS5_VABI sceNpTusSetMultiSlotVariableAsync(int reqId, const void* targetNpId, const std::int32_t* slotIds, const std::int64_t* variableArray, int arrayLen, void* option) { + (void)targetNpId; + if (!variableArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (const int error = ValidateSlots(slotIds, arrayLen, option)) return error; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusSetMultiSlotVariableVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, const std::int64_t* variableArray, int arrayLen, void* option) { + if (!targetVirtualUserId || !variableArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (const int error = ValidateSlots(slotIds, arrayLen, option)) return error; + if (IsEmpty(targetVirtualUserId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusDeleteMultiSlotVariableA(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, int arrayLen, void* option) { + return Complete(reqId, sceNpTusDeleteMultiSlotVariableAAsync(reqId, targetAccountId, slotIds, arrayLen, option)); +} + +int APS5_VABI sceNpTusDeleteMultiSlotVariableAsync(int reqId, const void* targetNpId, const std::int32_t* slotIds, int arrayLen, void* option) { + if (!targetNpId) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (const int error = ValidateNonEmptySlots(slotIds, arrayLen, option)) return error; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusDeleteMultiSlotVariable(int reqId, const void* targetNpId, const std::int32_t* slotIds, int arrayLen, void* option) { + return Complete(reqId, sceNpTusDeleteMultiSlotVariableAsync(reqId, targetNpId, slotIds, arrayLen, option)); +} + +int APS5_VABI sceNpTusDeleteMultiSlotVariableVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, int arrayLen, void* option) { + if (!targetVirtualUserId) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (const int error = ValidateNonEmptySlots(slotIds, arrayLen, option)) return error; + if (IsEmpty(targetVirtualUserId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return StartOffline(reqId); +} + +int APS5_VABI sceNpTusDeleteMultiSlotVariableVUser(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, int arrayLen, void* option) { + return Complete(reqId, sceNpTusDeleteMultiSlotVariableVUserAsync(reqId, targetVirtualUserId, slotIds, arrayLen, option)); +} + +int APS5_VABI sceNpTusGetMultiUserVariable(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserVariableVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserVariableAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserVariableVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetFriendsVariable(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetFriendsVariableAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusAddAndGetVariable(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusAddAndGetVariableVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusAddAndGetVariableAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusAddAndGetVariableVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusTryAndSetVariable(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusTryAndSetVariableVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusTryAndSetVariableAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusTryAndSetVariableVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusDeleteMultiSlotData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusDeleteMultiSlotDataVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusDeleteMultiSlotDataAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusDeleteMultiSlotDataVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusSetDataAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusSetDataVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetFriendsDataStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetFriendsDataStatusAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTssGetSmallStorage(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTssGetSmallStorageAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTssGetStorage(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTssGetStorageAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTssGetDataAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusAddAndGetVariableA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusAddAndGetVariableAVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusAddAndGetVariableAVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusAddAndGetVariableForCrossSave(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusAddAndGetVariableForCrossSaveAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusAddAndGetVariableForCrossSaveVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusAddAndGetVariableForCrossSaveVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusAAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusAVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusAVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSave(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSaveAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSaveVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSaveVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataAAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataAVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataAVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataForCrossSave(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataForCrossSaveAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataForCrossSaveVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataForCrossSaveVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserVariableA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserVariableAAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserVariableAVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserVariableAVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserVariableForCrossSave(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserVariableForCrossSaveAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserVariableForCrossSaveVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserVariableForCrossSaveVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusAAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusAVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusAVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSave(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSaveAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSaveVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSaveVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetFriendsVariableA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetFriendsVariableAAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetFriendsVariableForCrossSave(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetFriendsVariableForCrossSaveAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetFriendsDataStatusA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetFriendsDataStatusAAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetFriendsDataStatusForCrossSave(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetFriendsDataStatusForCrossSaveAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusSetDataA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusSetDataAAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusSetDataAVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusSetDataAVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusDeleteMultiSlotDataA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusDeleteMultiSlotDataAAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusTryAndSetVariableA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusTryAndSetVariableAAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusTryAndSetVariableAVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusTryAndSetVariableAVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusTryAndSetVariableForCrossSave(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusTryAndSetVariableForCrossSaveAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusTryAndSetVariableForCrossSaveVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusTryAndSetVariableForCrossSaveVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceNpUniversalDataSystem/Export.cpp b/core/libs/prx/libSceNpUniversalDataSystem/Export.cpp new file mode 100644 index 0000000000..0231b0cf03 --- /dev/null +++ b/core/libs/prx/libSceNpUniversalDataSystem/Export.cpp @@ -0,0 +1,63 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceNpUniversalDataSystemIntAbortHandle(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUniversalDataSystemIntCheckNetSyncTitles(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUniversalDataSystemIntCreateContext(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUniversalDataSystemIntCreateHandle(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUniversalDataSystemIntDeleteAllData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUniversalDataSystemIntDeleteUserDataWithServerData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUniversalDataSystemIntDestroyContext(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUniversalDataSystemIntDestroyHandle(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUniversalDataSystemIntInitialize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUniversalDataSystemIntNetSyncTitles(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpUniversalDataSystemIntTerminate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceNpUniversalDataSystem/src/UdsEvent.cpp b/core/libs/prx/libSceNpUniversalDataSystem/src/UdsEvent.cpp index 201178ddbf..94767a7d35 100644 --- a/core/libs/prx/libSceNpUniversalDataSystem/src/UdsEvent.cpp +++ b/core/libs/prx/libSceNpUniversalDataSystem/src/UdsEvent.cpp @@ -1,8 +1,10 @@ #include "prx/libSceNpUniversalDataSystem/include/UdsEvent.hpp" #include "prx/libSceNpTrophy2/include/TrophyStore.hpp" +#include #include #include +#include #include namespace Uds { @@ -34,6 +36,14 @@ void appendQuoted(std::string& out, std::string_view text) { void appendValue(std::string& out, const Value& value); +template +void appendFormattedNumber(std::string& out, T value, int precision) { + char number[40]; + const auto result = std::to_chars(number, number + sizeof(number), value, std::chars_format::general, precision); + if (result.ec != std::errc{}) throw std::runtime_error("event property number formatting failed"); + out.append(number, static_cast(result.ptr - number)); +} + void appendObject(std::string& out, const NpUniversalDataSystemEventPropertyObject& object) { out += '{'; bool first = true; @@ -60,11 +70,9 @@ void appendValue(std::string& out, const Value& value) { std::snprintf(number, sizeof(number), "%llu", static_cast(held)); out += number; } else if constexpr (std::is_same_v) { - std::snprintf(number, sizeof(number), "%.9g", static_cast(held)); - out += number; + appendFormattedNumber(out, held, 9); } else if constexpr (std::is_same_v) { - std::snprintf(number, sizeof(number), "%.17g", held); - out += number; + appendFormattedNumber(out, held, 17); } else if constexpr (std::is_same_v) { appendQuoted(out, held); } else if constexpr (std::is_same_v) { diff --git a/core/libs/prx/libSceNpUniversalDataSystem/tests/Events.cpp b/core/libs/prx/libSceNpUniversalDataSystem/tests/Events.cpp index d9cca747e4..73128d5ff0 100644 --- a/core/libs/prx/libSceNpUniversalDataSystem/tests/Events.cpp +++ b/core/libs/prx/libSceNpUniversalDataSystem/tests/Events.cpp @@ -2,6 +2,7 @@ #include "prx/libSceNpTrophy2/include/TrophyStore.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include @@ -231,6 +232,40 @@ void testStatEvent() { } } +void testLocaleIndependentNumberFormatting() { + const char* previous = std::setlocale(LC_NUMERIC, nullptr); + check(previous != nullptr, "the current locale could not be read"); + const std::string saved = previous; + struct RestoreLocale { + std::string saved; + ~RestoreLocale() { std::setlocale(LC_NUMERIC, saved.c_str()); } + } restore{saved}; + bool commaDecimal = false; + for (const auto* name : {"French", "French_France.1252", "fr_FR.UTF-8", "de_DE.UTF-8"}) { + if (std::setlocale(LC_NUMERIC, name) != nullptr && std::string(std::localeconv()->decimal_point) == ",") { + commaDecimal = true; + break; + } + } + if (!commaDecimal) { +#ifdef _WIN32 + throw std::runtime_error("No Windows comma-decimal locale available for regression"); +#else + std::puts("Comma-decimal locale regression unavailable on this host"); + return; +#endif + } + NpUniversalDataSystemEvent event; + event.properties.Set("float", 0.1f); + event.properties.Set("double", 0.1); + check(Uds::Describe(event.properties) == R"({"float":0.100000001,"double":0.10000000000000001})", + "described event properties changed with the numeric locale"); + const auto converted = Uds::StatEvent(event); + check(converted.properties.size() == 2, "float and double event properties were lost"); + check(converted.properties[0].text == "0.100000001", "float property text changed with the numeric locale"); + check(converted.properties[1].text == "0.10000000000000001", "double property text changed with the numeric locale"); +} + struct Unlocks { int count = 0; }; @@ -286,6 +321,7 @@ int main() { testEvents(); testRecognition(); testStatEvent(); + testLocaleIndependentNumberFormatting(); testPostingIgnoredEvents(); std::puts("UDS event tests passed"); return 0; diff --git a/core/libs/prx/libSceNpWebApi2/Export.cpp b/core/libs/prx/libSceNpWebApi2/Export.cpp index 1157934220..bb51fabeaa 100644 --- a/core/libs/prx/libSceNpWebApi2/Export.cpp +++ b/core/libs/prx/libSceNpWebApi2/Export.cpp @@ -22,6 +22,13 @@ struct NpWebApi2MemoryPoolStats { static std::mutex g_poolsMutex; static std::unordered_map g_pools; +static int CreateLibraryContext(size_t poolSize) { + const int libCtxId = g_nextHandle.fetch_add(1, std::memory_order_relaxed); + std::lock_guard lock(g_poolsMutex); + g_pools[libCtxId] = poolSize; + return libCtxId; +} + extern "C" { int APS5_VABI sceNpWebApi2AbortRequest(int64_t request_id) { @@ -88,10 +95,7 @@ int APS5_VABI sceNpWebApi2GetHttpResponseHeaderValueLength(int64_t request_id, c int APS5_VABI sceNpWebApi2Initialize(int lib_http_ctx_id, size_t pool_size) { (void)lib_http_ctx_id; - const int lib_ctx_id = g_nextHandle.fetch_add(1, std::memory_order_relaxed); - std::lock_guard lock(g_poolsMutex); - g_pools[lib_ctx_id] = pool_size; - return lib_ctx_id; + return CreateLibraryContext(pool_size); } int APS5_VABI sceNpWebApi2GetMemoryPoolStats(int lib_ctx_id, NpWebApi2MemoryPoolStats* stats) { @@ -204,4 +208,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) { + (void)lib_http_ctx_id; + return CreateLibraryContext(pool_size); +} + +int APS5_VABI sceNpWebApi2IntCreateRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApi2IntInitialize(const NpWebApi2IntInitializeArgs* args) { + if (args == nullptr || args->struct_size != sizeof(NpWebApi2IntInitializeArgs)) return SCE_NP_WEBAPI2_ERROR_INVALID_ARGUMENT; + return CreateLibraryContext(args->pool_size); +} + +int APS5_VABI sceNpWebApi2IntInitialize2(const NpWebApi2IntInitialize2Args* args) { + if (args == nullptr || args->struct_size != sizeof(NpWebApi2IntInitialize2Args)) return SCE_NP_WEBAPI2_ERROR_INVALID_ARGUMENT; + return CreateLibraryContext(args->pool_size); +} + +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 d66f710d7a..362519358f 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -15,8 +15,6 @@ namespace { constexpr int PAD_ERROR_DEVICE_NOT_CONNECTED = static_cast(0x80920007); constexpr int PAD_ERROR_DEVICE_NO_HANDLE = static_cast(0x80920008); -constexpr int PAD_UNKNOWN_N3KSX62FGNO_BEFORE_INIT = static_cast(0x80920005); -constexpr int PAD_UNKNOWN_N3KSX62FGNO_AFTER_INIT = static_cast(0x809200ff); bool g_opened = false; std::atomic_bool g_initialized{false}; @@ -80,8 +78,8 @@ int APS5_VABI scePadGetControllerInformation(int handle, PadControllerInformatio } std::memset(info, 0, sizeof(*info)); info->touchPadInfo.pixelDensity = 44.86f; - info->touchPadInfo.resolution.x = 1920; - info->touchPadInfo.resolution.y = 943; + info->touchPadInfo.resolution.x = kTouchPadWidth; + info->touchPadInfo.resolution.y = kTouchPadHeight; info->stickInfo.deadZoneLeft = 2; info->stickInfo.deadZoneRight = 2; info->connectionType = PAD_CONNECTION_TYPE_LOCAL; @@ -124,8 +122,8 @@ int APS5_VABI scePadGetExtControllerInformation(int handle, void* info) { std::memset(info, 0, 0x2c); PadControllerInformation* base = static_cast(info); base->touchPadInfo.pixelDensity = 44.86f; - base->touchPadInfo.resolution.x = 1920; - base->touchPadInfo.resolution.y = 943; + base->touchPadInfo.resolution.x = kTouchPadWidth; + base->touchPadInfo.resolution.y = kTouchPadHeight; base->stickInfo.deadZoneLeft = 2; base->stickInfo.deadZoneRight = 2; base->connectionType = PAD_CONNECTION_TYPE_LOCAL; @@ -179,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(); @@ -317,8 +315,7 @@ int APS5_VABI scePadSetAngularVelocityBiasCorrectionState(int handle, bool enabl APS5_EXPORT("n3kSX62fgNo", scePadUnknown_n3kSX62fgNo); int APS5_VABI scePadUnknown_n3kSX62fgNo() { - if (g_opened) NotImplemented_nid_no_patch(__func__); - return g_initialized.load(std::memory_order_acquire) ? PAD_UNKNOWN_N3KSX62FGNO_AFTER_INIT : PAD_UNKNOWN_N3KSX62FGNO_BEFORE_INIT; + return g_initialized.load(std::memory_order_acquire) ? PAD_OK : PAD_ERROR_NOT_INITIALIZED; } int APS5_VABI scePadVrControllerGetTriggerEffectStates(void) { @@ -326,10 +323,12 @@ int APS5_VABI scePadVrControllerGetTriggerEffectStates(void) { return 0; } -APS5_EXPORT("zBsgF0q8DIM", scePadUnknown_zBsgF0q8DIM); -int APS5_VABI scePadUnknown_zBsgF0q8DIM() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI scePadGetAccessibilityControllerState(int handle, std::uint32_t* state) { + if (!Pad::IsInitialized()) return PAD_ERROR_NOT_INITIALIZED; + if (state == nullptr) return PAD_ERROR_INVALID_ARG; + if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; + *state = 0; + return PAD_OK; } int APS5_VABI scePadSetTriggerEffectExt(void) { @@ -358,7 +357,6 @@ int APS5_VABI scePadGetExtensionUnitInfo(void) { } int APS5_VABI scePadEnableExtensionPort(void) { - NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libScePad/include/InputCommandFile.hpp b/core/libs/prx/libScePad/include/InputCommandFile.hpp index 5ee3b1fb73..e372a1f3f7 100644 --- a/core/libs/prx/libScePad/include/InputCommandFile.hpp +++ b/core/libs/prx/libScePad/include/InputCommandFile.hpp @@ -1,13 +1,18 @@ #ifndef CORE_LIBS_PRX_LIBSCEPAD_INPUTCOMMANDFILE_HPP #define CORE_LIBS_PRX_LIBSCEPAD_INPUTCOMMANDFILE_HPP +#include #include #include #include #include +#include +#include #include #include #include +#include +#include namespace Pad { class InputCommandFile { @@ -29,24 +34,36 @@ public: Input Poll(std::uint64_t now) { if (path.empty()) return {}; if (now >= nextPoll) { - nextPoll = now + 100000; - const auto command = read(); + nextPoll = now > std::numeric_limits::max() - 100000 + ? std::numeric_limits::max() : now + 100000; + auto command = read(); if (command.sequence > lastSequence) { lastSequence = command.sequence; - input = command.input; - until = now + command.durationMs * 1000ull; + events = std::move(command.events); + started = now; } } - return now < until ? input : Input{}; + if (events.empty() || now < started) return {}; + const auto elapsed = now - started; + const auto next = std::upper_bound(events.begin(), events.end(), elapsed, + [](std::uint64_t time, const Event& event) { return time < event.start; }); + if (next == events.begin()) return {}; + const auto& event = *std::prev(next); + return elapsed < event.end ? event.input : Input{}; } std::uint32_t Buttons(std::uint64_t now) { return Poll(now).buttons; } private: + struct Event { + std::uint64_t start = 0; + std::uint64_t end = 0; + Input input; + }; + struct Command { std::uint64_t sequence = 0; - Input input; - std::uint32_t durationMs = 0; + std::vector events; }; Command read() const { @@ -56,6 +73,40 @@ private: const std::string text(line); if (text.find('-') != std::string::npos) return {}; std::istringstream fields(text); + std::string first; + if (!(fields >> first)) return {}; + if (first == "APS5_PAD_TIMELINE") { + Command command; + std::string extra; + std::uint64_t version = 0, count = 0; + if (!(fields >> version >> command.sequence >> count) || fields >> extra) return {}; + if (version != 1 || command.sequence == 0 || count == 0 || count > 4096) return {}; + if (command.sequence <= lastSequence) return command; + command.events.reserve(static_cast(count)); + std::uint64_t previousEnd = 0; + for (std::uint64_t index = 0; index < count; ++index) { + if (!file.getline(line, sizeof(line))) return {}; + const std::string eventText(line); + if (eventText.find('-') != std::string::npos) return {}; + fields.clear(); + fields.str(eventText); + std::uint64_t start = 0, duration = 0, buttons = 0; + if (!(fields >> start >> duration >> buttons) || fields >> extra) return {}; + if ((buttons & ~(0x10fffeull | TouchLeftBit | TouchRightBit)) != 0 || duration == 0 || duration > 5000 || + start < previousEnd || start > 3600000 - duration) return {}; + previousEnd = start + duration; + Input input; + input.buttons = static_cast(buttons & 0x10fffeull); + input.touchLeft = (buttons & TouchLeftBit) != 0; + input.touchRight = (buttons & TouchRightBit) != 0; + command.events.push_back({start * 1000, previousEnd * 1000, input}); + } + file >> std::ws; + if (file.peek() != std::char_traits::eof()) return {}; + return command; + } + fields.clear(); + fields.str(text); std::string tokens[8]; int count = 0; while (count < 8 && fields >> tokens[count]) ++count; @@ -70,15 +121,16 @@ private: if (values[0] == 0 || (values[1] & ~(0x10fffeull | TouchLeftBit | TouchRightBit)) != 0 || values[2] == 0 || values[2] > 5000) return {}; Command command; command.sequence = values[0]; - command.input.buttons = static_cast(values[1] & 0x10fffeull); - command.input.touchLeft = (values[1] & TouchLeftBit) != 0; - command.input.touchRight = (values[1] & TouchRightBit) != 0; - command.durationMs = static_cast(values[2]); + Input input; + input.buttons = static_cast(values[1] & 0x10fffeull); + input.touchLeft = (values[1] & TouchLeftBit) != 0; + input.touchRight = (values[1] & TouchRightBit) != 0; for (int i = 3; i < count; ++i) { if (values[i] > 255) return {}; - command.input.sticks[i - 3] = static_cast(values[i]); + input.sticks[i - 3] = static_cast(values[i]); } - command.input.hasSticks = count == 7; + input.hasSticks = count == 7; + command.events.push_back({0, values[2] * 1000, input}); return command; } @@ -88,8 +140,8 @@ private: std::filesystem::path path; std::uint64_t lastSequence = 0; std::uint64_t nextPoll = 0; - std::uint64_t until = 0; - Input input; + std::uint64_t started = 0; + std::vector events; }; } 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/libScePad/include/PadState.hpp b/core/libs/prx/libScePad/include/PadState.hpp index 8186ee492a..fb621171ee 100644 --- a/core/libs/prx/libScePad/include/PadState.hpp +++ b/core/libs/prx/libScePad/include/PadState.hpp @@ -6,6 +6,9 @@ #include #include "SceTypes.hpp" +constexpr std::uint16_t kTouchPadWidth = 1920; +constexpr std::uint16_t kTouchPadHeight = 1080; + struct PadTouchPoint { bool active = false; std::uint16_t x = 0; diff --git a/core/libs/prx/libScePad/src/PadState.cpp b/core/libs/prx/libScePad/src/PadState.cpp index 1aef805ccb..5a6795f32e 100644 --- a/core/libs/prx/libScePad/src/PadState.cpp +++ b/core/libs/prx/libScePad/src/PadState.cpp @@ -15,6 +15,11 @@ namespace { std::mutex stateMutex; PadInputState state; + std::uint32_t pressedSinceRead = 0; + std::uint8_t pressedAnalogL2 = 0; + std::uint8_t pressedAnalogR2 = 0; + std::uint64_t pressedAt = 0; + constexpr std::uint64_t kLatchExpiry = 100000; PadOutputState output; std::chrono::steady_clock::time_point audioHapticEnd; std::array audioHapticRumble{}; @@ -122,6 +127,7 @@ void Pad::Initialize() { timestamp = sceKernelGetProcessTime(); lastFuseTime = timestamp; queue.clear(); + pressedSinceRead = 0; } initialized = true; } @@ -196,8 +202,8 @@ PadData BuildData(const PadInputState& state, std::uint64_t changeTime, std::uin if (touchNum == 0 && (state.touchLeft || state.touchRight || injected.touchLeft || injected.touchRight)) { data.buttons |= 0x100000; touchNum = 1; - data.touch_data_touch0_x = state.touchRight || injected.touchRight ? 1440 : 480; - data.touch_data_touch0_y = 471; + data.touch_data_touch0_x = state.touchRight || injected.touchRight ? kTouchPadWidth * 3 / 4 : kTouchPadWidth / 4; + data.touch_data_touch0_y = kTouchPadHeight / 2; data.touch_data_touch0_id = 0; } data.touch_data_touch_num = touchNum; @@ -229,7 +235,14 @@ PadData Pad::ReadState() { if (!initialized) throw std::runtime_error("Pad: read before initialization"); const std::uint64_t now = sceKernelGetProcessTime(); const auto injected = InjectedInput(now); - return BuildData(state, timestamp, now, injected); + PadData data = BuildData(state, timestamp, now, injected); + if (pressedSinceRead != 0 && now - pressedAt <= kLatchExpiry) { + data.buttons |= pressedSinceRead; + if (pressedSinceRead & 0x100u) data.analog_buttons_l2 = std::max(data.analog_buttons_l2, pressedAnalogL2); + if (pressedSinceRead & 0x200u) data.analog_buttons_r2 = std::max(data.analog_buttons_r2, pressedAnalogR2); + } + pressedSinceRead = 0; + return data; } int Pad::Read(PadData* data, int num) { @@ -237,6 +250,7 @@ int Pad::Read(PadData* data, int num) { std::lock_guard lock(stateMutex); if (failure) std::rethrow_exception(failure); if (!initialized) throw std::runtime_error("Pad: read before initialization"); + pressedSinceRead = 0; const std::uint64_t now = sceKernelGetProcessTime(); const auto injected = InjectedInput(now); if (queue.empty()) { @@ -371,8 +385,16 @@ extern "C" void PadPublishInput_nid_postfix(const PadInputState& input) { state.touchLeft == input.touchLeft && state.touchRight == input.touchRight && state.hasMotion == input.hasMotion && state.accel == input.accel && state.gyro == input.gyro && state.touch == input.touch && state.deviceKind == input.deviceKind) return; - state = input; + const std::uint32_t pressed = input.buttons & ~state.buttons; timestamp = sceKernelGetProcessTime(); + if (pressed != 0) { + if (pressedSinceRead != 0 && timestamp - pressedAt > kLatchExpiry) pressedSinceRead = 0; + pressedSinceRead |= pressed; + pressedAt = timestamp; + if (pressed & 0x100u) pressedAnalogL2 = input.analogButtonsL2; + if (pressed & 0x200u) pressedAnalogR2 = input.analogButtonsR2; + } + state = input; if (queue.size() == kQueueCapacity) queue.pop_front(); queue.push_back({input, timestamp}); } diff --git a/core/libs/prx/libScePad/tests/InputCommandFile.cpp b/core/libs/prx/libScePad/tests/InputCommandFile.cpp new file mode 100644 index 0000000000..64309a4892 --- /dev/null +++ b/core/libs/prx/libScePad/tests/InputCommandFile.cpp @@ -0,0 +1,111 @@ +#include "prx/libScePad/include/InputCommandFile.hpp" +#include +#include +#include +#include +#include +#include + +void Check(bool condition) { + if (!condition) throw std::runtime_error("input command assertion failed"); +} + +int main() { + const auto path = std::filesystem::temp_directory_path() / + ("anyps5-pad-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + const auto write = [&](const std::string& text) { std::ofstream(path) << text; }; + try { + write("1 16384 300\n"); + Pad::InputCommandFile input(path); + Check(input.Buttons(0) == 0); + write("APS5_PAD_TIMELINE 1 2 3\n200 100 18432\n500 200 16\n800 100 0\n"); + Check(input.Buttons(100000) == 0); + Check(input.Buttons(299999) == 0); + Check(input.Buttons(300000) == 18432); + Check(input.Buttons(399999) == 18432); + Check(input.Buttons(400000) == 0); + Check(input.Buttons(600000) == 16); + Check(input.Buttons(800000) == 0); + Check(input.Buttons(1100000) == 0); + write("APS5_PAD_TIMELINE 1 3 2\n0 500 32768\n300 100 16\n"); + Check(input.Buttons(1200000) == 0); + write("APS5_PAD_TIMELINE 1 3 2\n0 500 32768\n600 100 16\n"); + Check(input.Buttons(1300000) == 32768); + write("APS5_PAD_TIMELINE 1 4 1\n0 6000 16\n"); + Check(input.Buttons(1400000) == 32768); + write("APS5_PAD_TIMELINE 1 4 1\n0 500 16\nextra\n"); + Check(input.Buttons(1500000) == 32768); + write("APS5_PAD_TIMELINE 1 4 2\n0 100 16\n"); + Check(input.Buttons(1600000) == 32768); + write("APS5_PAD_TIMELINE 2 4 1\n0 100 16\n"); + Check(input.Buttons(1700000) == 32768); + Check(input.Buttons(1800000) == 0); + write("4 8 200\n"); + Check(input.Buttons(1900000) == 8); + Check(input.Buttons(2100000) == 0); + write("3 16 200\n"); + Check(input.Buttons(2200000) == 0); + write("5 0 1\n"); + Check(input.Buttons(2300000) == 0); + const std::string bad[] = { + "APS5_PAD_TIMELINE 1 6 0\n", "APS5_PAD_TIMELINE 1 6 4097\n", + "APS5_PAD_TIMELINE 1 6 1\n0 1 1\n", "APS5_PAD_TIMELINE 1 6 1\n-1 1 16\n", + "APS5_PAD_TIMELINE 1 6 1\n3600000 1 16\n", + "APS5_PAD_TIMELINE 1 18446744073709551616 1\n0 1 16\n", + "APS5_PAD_TIMELINE 1 6 1\n18446744073709551615 1 16\n", + "APS5_PAD_TIMELINE 1 6 1\n0 1 4294967296\n" + }; + std::uint64_t clock = 2400000; + for (const auto& text : bad) { + write(text); + Check(input.Buttons(clock) == 0); + clock += 100000; + } + write("APS5_PAD_TIMELINE 1 6 1\n3599999 1 16384\n"); + Check(input.Buttons(clock) == 0); + Check(input.Buttons(clock + 3599999000ull) == 16384); + Check(input.Buttons(clock + 3600000000ull) == 0); + write("APS5_PAD_TIMELINE 1 7 1\n0 100 16\n"); + const auto maximum = std::numeric_limits::max(); + Check(input.Buttons(maximum - 10) == 16); + Check(input.Buttons(maximum) == 16); + Pad::InputCommandFile stale(path); + Check(stale.Buttons(0) == 0); + Pad::InputCommandFile disabled(std::filesystem::path{}); + Check(disabled.Buttons(0) == 0); + write("10 16384 300 0 255 128 64\n"); + Pad::InputCommandFile sticks(path); + Check(sticks.Poll(0) == Pad::InputCommandFile::Input{}); + write("11 16 200 0 255 128 64\n"); + Check(sticks.Poll(100000) == Pad::InputCommandFile::Input{16, true, {0, 255, 128, 64}}); + write("12 16 200 0 256 128 64\n"); + Check(sticks.Poll(200000) == Pad::InputCommandFile::Input{16, true, {0, 255, 128, 64}}); + Check(sticks.Poll(300000) == Pad::InputCommandFile::Input{}); + write("12 16 200 0 255 128 64 1\n"); + Check(sticks.Poll(400000) == Pad::InputCommandFile::Input{}); + write("APS5_PAD_TIMELINE 1 12 1\n0 500 16384\n"); + Check(sticks.Poll(500000) == Pad::InputCommandFile::Input{16384}); + write("13 8 200 255 0 64 128\n"); + Check(sticks.Poll(600000) == Pad::InputCommandFile::Input{8, true, {255, 0, 64, 128}}); + write("14 16 100\n"); + Check(sticks.Poll(700000) == Pad::InputCommandFile::Input{16}); + Check(sticks.Poll(800000) == Pad::InputCommandFile::Input{}); + write("15 2113536 100 0 255 128 64\n"); + Check(sticks.Poll(900000) == Pad::InputCommandFile::Input{16384, true, {0, 255, 128, 64}, true, false}); + Check(sticks.Poll(1000000) == Pad::InputCommandFile::Input{}); + write("APS5_PAD_TIMELINE 1 16 3\n0 100 2113536\n100 100 4194304\n200 100 6291456\n"); + Check(sticks.Poll(1100000) == Pad::InputCommandFile::Input{16384, false, {}, true, false}); + Check(sticks.Poll(1200000) == Pad::InputCommandFile::Input{0, false, {}, false, true}); + Check(sticks.Poll(1300000) == Pad::InputCommandFile::Input{0, false, {}, true, true}); + Check(sticks.Poll(1400000) == Pad::InputCommandFile::Input{}); + write("APS5_PAD_TIMELINE 1 17 1\n0 100 8388608\n"); + Check(sticks.Poll(1500000) == Pad::InputCommandFile::Input{}); + std::filesystem::remove(path); + std::cout << "Controller timeline runtime cases passed\n"; + return 0; + } catch (const std::exception& error) { + std::filesystem::remove(path); + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libScePlayGo/Export.cpp b/core/libs/prx/libScePlayGo/Export.cpp index 2bc5bfc67c..576b4c4f0c 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; @@ -40,8 +41,8 @@ static bool ReadPackageChunks(ChunkTable& table) { std::ifstream file(ResolvePath_nid_no_patch("/app0/sce_sys/playgo-chunk.dat"), std::ios::binary); char header[CountOffset + sizeof(uint16_t)]{}; if (!file) return false; - const auto unreadable = [] { return std::runtime_error("scePlayGo: sce_sys/playgo-chunk.dat is not a chunk table this library reads (expected a plgx header with a nonzero chunk count)"); }; - if (!file.read(header, sizeof(header)) || std::memcmp(header, "plgx", 4) != 0) throw unreadable(); + const auto unreadable = [] { return std::runtime_error("scePlayGo: sce_sys/playgo-chunk.dat is not a chunk table this library reads (expected a plgx or plgo header with a nonzero chunk count)"); }; + if (!file.read(header, sizeof(header)) || (std::memcmp(header, "plgx", 4) != 0 && std::memcmp(header, "plgo", 4) != 0)) throw unreadable(); uint16_t count = 0; std::memcpy(&count, header + CountOffset, sizeof(count)); if (count == 0) throw unreadable(); @@ -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/libScePlayGoDialog/Export.cpp b/core/libs/prx/libScePlayGoDialog/Export.cpp index 09a8df031c..65b0b8eba1 100644 --- a/core/libs/prx/libScePlayGoDialog/Export.cpp +++ b/core/libs/prx/libScePlayGoDialog/Export.cpp @@ -20,6 +20,7 @@ constexpr std::int32_t ERROR_NOT_INITIALIZED = static_cast(0x80ED0 constexpr std::int32_t ERROR_ALREADY_INITIALIZED = static_cast(0x80ED0002); constexpr std::int32_t ERROR_PARAM_INVALID = static_cast(0x80ED0003); constexpr std::int32_t ERROR_INVALID_STATE = static_cast(0x80ED0005); +constexpr std::int32_t COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast(0x80B80005u); std::atomic g_status{STATUS_NONE}; @@ -67,6 +68,7 @@ int APS5_VABI scePlayGoDialogUpdateStatus(void) { int APS5_VABI scePlayGoDialogGetResult(void* result) { if (g_status.load() == STATUS_NONE) return ERROR_NOT_INITIALIZED; if (!result) return ERROR_PARAM_INVALID; + if (g_status.load() != STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED; auto* r = static_cast(result); std::memset(r, 0, sizeof(*r)); r->result = RESULT_OK; diff --git a/core/libs/prx/libScePlayerSelectionDialog/CMakeLists.txt b/core/libs/prx/libScePlayerSelectionDialog/CMakeLists.txt index 02e53fdc16..670b5b3b0a 100644 --- a/core/libs/prx/libScePlayerSelectionDialog/CMakeLists.txt +++ b/core/libs/prx/libScePlayerSelectionDialog/CMakeLists.txt @@ -5,7 +5,8 @@ configure_windows_unwind(libScePlayerSelectionDialog) if(BUILD_TESTING) add_test_executable(guest_player_selection_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestPlayerSelectionDialog.cpp) target_include_directories(guest_player_selection_dialog_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_player_selection_dialog_tests PRIVATE libScePlayerSelectionDialog) + target_link_libraries(guest_player_selection_dialog_tests PRIVATE libScePlayerSelectionDialog libc) + configure_windows_unwind(guest_player_selection_dialog_tests) add_test(NAME guest_player_selection_dialog COMMAND guest_player_selection_dialog_tests) if(WIN32) set_tests_properties(guest_player_selection_dialog PROPERTIES ENVIRONMENT_MODIFICATION diff --git a/core/libs/prx/libScePlayerSelectionDialog/Export.cpp b/core/libs/prx/libScePlayerSelectionDialog/Export.cpp index d866a19fe2..4f9c50b9a7 100644 --- a/core/libs/prx/libScePlayerSelectionDialog/Export.cpp +++ b/core/libs/prx/libScePlayerSelectionDialog/Export.cpp @@ -63,6 +63,11 @@ int APS5_VABI scePlayerSelectionDialogGetResult(void* result) { return 0; } +int APS5_VABI scePlayerSelectionDialogParamInitialize(void* param) { + if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + NotImplemented_nid_no_patch(__func__); +} + int APS5_VABI scePlayerSelectionDialogTerminate(void) { if (g_status.exchange(COMMON_DIALOG_STATUS_NONE) == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; return 0; diff --git a/core/libs/prx/libScePngDec/Export.cpp b/core/libs/prx/libScePngDec/Export.cpp index 220f2aaeec..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); @@ -57,7 +58,7 @@ std::int32_t validateCreateParam(const PngDecCreateParam* param) { if (!param) return SCE_PNG_DEC_ERROR_INVALID_PARAM; if (param->this_size != sizeof(PngDecCreateParam)) return SCE_PNG_DEC_ERROR_INVALID_SIZE; if (param->attribute != ATTRIBUTE_NONE && param->attribute != ATTRIBUTE_BIT_DEPTH_16) return SCE_PNG_DEC_ERROR_INVALID_PARAM; - if (param->max_image_width == 0 || param->max_image_width - 1 > MAX_IMAGE_WIDTH) return SCE_PNG_DEC_ERROR_INVALID_SIZE; + if (param->max_image_width - 1 >= MAX_IMAGE_WIDTH) return SCE_PNG_DEC_ERROR_INVALID_PARAM; return 0; } @@ -124,21 +125,30 @@ int32_t APS5_VABI scePngDecDecode(void* handle, const PngDecDecodeParam* param, const Context* context = toContext(handle); if (!context) return SCE_PNG_DEC_ERROR_INVALID_HANDLE; if (!param) return SCE_PNG_DEC_ERROR_INVALID_PARAM; - if (!param->png_mem_addr || !param->image_mem_addr) return SCE_PNG_DEC_ERROR_INVALID_ADDR; + if (!param->png_mem_addr || !param->image_mem_addr || (reinterpret_cast(param->image_mem_addr) & 3u) != 0) return SCE_PNG_DEC_ERROR_INVALID_ADDR; if (param->png_mem_size == 0 || param->image_mem_size == 0) return SCE_PNG_DEC_ERROR_INVALID_SIZE; if (param->pixel_format != PIXEL_FORMAT_R8G8B8A8 && param->pixel_format != PIXEL_FORMAT_B8G8R8A8) return SCE_PNG_DEC_ERROR_INVALID_PARAM; + if (param->alpha_value > 0xFF) return SCE_PNG_DEC_ERROR_INVALID_PARAM; const std::span png = toBytes(param->png_mem_addr, param->png_mem_size); const std::optional header = Decoder::Png::ParseHeader(png); if (!header) return SCE_PNG_DEC_ERROR_INVALID_DATA; + if (image_info) fillImageInfo(*header, image_info); if (header->bitDepth == 16 && context->attribute == ATTRIBUTE_BIT_DEPTH_16) { throw std::runtime_error("scePngDecDecode: 16-bit output is not implemented"); } const std::uint64_t rowSize = static_cast(header->width) * BYTES_PER_PIXEL; const std::uint64_t pitch = param->image_pitch == 0 ? rowSize : param->image_pitch; - if (pitch < rowSize) return SCE_PNG_DEC_ERROR_INVALID_PARAM; - if ((header->height - 1) * pitch + rowSize > param->image_mem_size) return SCE_PNG_DEC_ERROR_INVALID_SIZE; + if (param->image_pitch != 0 && (pitch < rowSize || (pitch & 3u) != 0)) return SCE_PNG_DEC_ERROR_INVALID_PARAM; + if (param->image_pitch == 0) { + if (header->height * rowSize > param->image_mem_size) return SCE_PNG_DEC_ERROR_INVALID_SIZE; + } else if (header->height * pitch >= 0x80000000ull || header->height * pitch > param->image_mem_size) { + return SCE_PNG_DEC_ERROR_INVALID_PARAM; + } + const std::uint64_t alignedWidth = (static_cast(header->width) + 7u) & ~std::uint64_t{7u}; + const std::uint64_t alignedMaxWidth = (static_cast(context->maxImageWidth) + 7u) & ~std::uint64_t{7u}; + if (alignedWidth > alignedMaxWidth) return SCE_PNG_DEC_ERROR_INVALID_WORK_MEMORY; const std::optional image = Decoder::Png::Decode(png); if (!image || image->width != header->width || image->height != header->height) return SCE_PNG_DEC_ERROR_DECODE_ERROR; @@ -158,7 +168,6 @@ int32_t APS5_VABI scePngDecDecode(void* handle, const PngDecDecodeParam* 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); } @@ -186,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/libSceRazorCpu/CMakeLists.txt b/core/libs/prx/libSceRazorCpu/CMakeLists.txt index 38dc075ae6..e4d7808ed9 100644 --- a/core/libs/prx/libSceRazorCpu/CMakeLists.txt +++ b/core/libs/prx/libSceRazorCpu/CMakeLists.txt @@ -6,6 +6,7 @@ if(BUILD_TESTING) add_test_executable(guest_razor_cpu_tests ${LIBS_INCLUDE_DIR}/tests/GuestRazorCpu.cpp) target_include_directories(guest_razor_cpu_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_razor_cpu_tests PRIVATE libSceRazorCpu) + configure_windows_unwind(guest_razor_cpu_tests) add_test(NAME guest_razor_cpu COMMAND guest_razor_cpu_tests) if(WIN32) set_tests_properties(guest_razor_cpu PROPERTIES ENVIRONMENT_MODIFICATION diff --git a/core/libs/prx/libSceRazorCpu/Export.cpp b/core/libs/prx/libSceRazorCpu/Export.cpp index 0714155d1b..af49b0fa3c 100644 --- a/core/libs/prx/libSceRazorCpu/Export.cpp +++ b/core/libs/prx/libSceRazorCpu/Export.cpp @@ -69,17 +69,16 @@ int APS5_VABI sceRazorCpuSync(void) { } void APS5_VABI sceRazorCpuDisableFiberUserMarkers(void) { - NotImplemented_nid_no_patch(__func__); } int APS5_VABI sceRazorCpuInitDataTags(void* buffer, size_t size) { (void)buffer; (void)size; - NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceRazorCpuShutdownDataTags(void) { - NotImplemented_nid_no_patch(__func__); + return 0; } size_t APS5_VABI sceRazorCpuGetDataTagStorageSize(uint32_t tagCount) { @@ -93,7 +92,7 @@ int APS5_VABI sceRazorCpuTagArray(const char* label, uint32_t category, void* ar (void)array; (void)bufferSize; (void)elementSize; - NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceRazorCpuTagBuffer(const char* label, uint32_t category, void* buffer, size_t bufferSize) { @@ -101,20 +100,20 @@ int APS5_VABI sceRazorCpuTagBuffer(const char* label, uint32_t category, void* b (void)category; (void)buffer; (void)bufferSize; - NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceRazorCpuResizeTaggedBuffer(uint32_t category, void* buffer, size_t newBufferSize) { (void)category; (void)buffer; (void)newBufferSize; - NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceRazorCpuUnTagBuffer(uint32_t category, void* buffer) { (void)category; (void)buffer; - NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceRazorCpuBeginLogicalFileAccess(const char* filename, uint64_t tag, uint64_t size, uint8_t operation) { @@ -122,11 +121,11 @@ int APS5_VABI sceRazorCpuBeginLogicalFileAccess(const char* filename, uint64_t t (void)tag; (void)size; (void)operation; - NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceRazorCpuEndLogicalFileAccess(void) { - NotImplemented_nid_no_patch(__func__); + return 0; } } diff --git a/core/libs/prx/libSceRemoteplay/Export.cpp b/core/libs/prx/libSceRemoteplay/Export.cpp index 1c3e5bbad0..bc653c840f 100644 --- a/core/libs/prx/libSceRemoteplay/Export.cpp +++ b/core/libs/prx/libSceRemoteplay/Export.cpp @@ -25,4 +25,199 @@ int APS5_VABI sceRemoteplayTerminate(void) { return 0; } +int APS5_VABI sceRemoteplayApprove(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayClearAllRegistData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayClearConnectHistory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayConfirmDeviceRegist(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayDisconnect(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayGeneratePinCode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayGetApMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayGetConnectHistory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayGetConnectUserId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayGetMbusDeviceInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayGetOperationStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayGetPrimaryUserId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayGetRemoteplayStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayGetRpMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayImeClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayImeFilterResult(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayImeGetEvent(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayImeNotify(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayImeNotifyEventResult(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayImeOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayImeSetCaret(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayImeSetText(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayIsRemoteOskReady(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayIsRemotePlaying(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayNotifyMbusDeviceRegistComplete(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayNotifyNpPushWakeup(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayNotifyPinCodeError(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayNotifySubmitCode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayNotifyUserDelete(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayPrintAllRegistData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayProhibit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayProhibitStreaming(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayServerLock(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplayServerUnLock(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplaySetApMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplaySetLogLevel(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplaySetProhibition(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplaySetProhibitionForVsh(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRemoteplaySetRpMode(void) { + 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..43d049c976 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; @@ -238,8 +274,8 @@ bool parseZoneName(const char* cursor, std::int64_t& offsetMinutes) { {"CAST", 570}, {"CADT", 630}, {"SAST", 570}, {"SADT", 630}, {"WST", 480}, {"WDT", 540}, {"GMT", 0}, {"WET", 0}, {"BST", 60}, {"WAT", -60}, {"AST", -240}, {"ADT", -180}, {"EST", -300}, {"EDT", -240}, {"CST", -360}, {"CDT", -300}, {"MST", -420}, {"MDT", -360}, {"PST", -480}, {"PDT", -420}, {"YST", -540}, {"YDT", -480}, - {"HDT", -540}, {"CAT", -600}, {"AHST", -600}, {"NST", -150}, {"NDT", -90}, {"IDLW", -720}, {"CET", 60}, {"MET", 60}, - {"MEWT", 60}, {"MEST", 120}, {"CEST", 120}, {"SST", 120}, {"FWT", 120}, {"FST", 60}, {"EET", 120}, {"IST", 120}, + {"HDT", -540}, {"CAT", -600}, {"AHST", -600}, {"NST", -150}, {"NFT", -150}, {"NDT", -90}, {"IDLW", -720}, {"CET", 60}, {"MET", 60}, + {"DNT", 60}, {"NOR", 60}, {"SET", 60}, {"MEWT", 60}, {"MEST", 120}, {"CEST", 120}, {"SST", 120}, {"FWT", 120}, {"FST", 60}, {"EET", 120}, {"IST", 120}, {"WAST", 420}, {"WADT", 480}, {"CCT", 480}, {"AWST", 480}, {"JST", 540}, {"KST", 540}, {"ACST", 570}, {"EAST", 600}, {"GST", 600}, {"AEST", 600}, {"NZT", 720}, {"NZST", 720}, {"NZDT", 780}, {"IDLE", 720}, {"JT", 450}, }; @@ -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) { @@ -536,7 +574,12 @@ int APS5_VABI sceRtcSetMicrosecond(RtcDateTime* time, int microsecond) { } int APS5_VABI sceRtcFormatRFC3339(char* date_time, const RtcTick* utc, int time_zone_minutes) { - if (!date_time || !utc) return SCE_RTC_ERROR_INVALID_POINTER; + if (!date_time) return SCE_RTC_ERROR_INVALID_POINTER; + RtcTick now{}; + if (!utc) { + now.tick = currentTick(); + utc = &now; + } if (time_zone_minutes < -1439 || time_zone_minutes > 1439) return SCE_RTC_ERROR_INVALID_VALUE; RtcTick local{}; if (const int result = addTicks(&local, utc, time_zone_minutes, TICKS_PER_MINUTE); result != 0) return result; @@ -552,13 +595,23 @@ int APS5_VABI sceRtcFormatRFC3339(char* date_time, const RtcTick* utc, int time_ } int APS5_VABI sceRtcFormatRFC3339LocalTime(char* date_time, const RtcTick* utc) { - if (!date_time || !utc) return SCE_RTC_ERROR_INVALID_POINTER; + if (!date_time) return SCE_RTC_ERROR_INVALID_POINTER; + RtcTick now{}; + if (!utc) { + now.tick = currentTick(); + utc = &now; + } if (utc->tick > MAX_TICK) return SCE_RTC_ERROR_INVALID_VALUE; return sceRtcFormatRFC3339(date_time, utc, localOffsetMinutes(*utc)); } int APS5_VABI sceRtcFormatRFC2822(char* date_time, const RtcTick* utc, int time_zone_minutes) { - if (!date_time || !utc) return SCE_RTC_ERROR_INVALID_POINTER; + if (!date_time) return SCE_RTC_ERROR_INVALID_POINTER; + RtcTick now{}; + if (!utc) { + now.tick = currentTick(); + utc = &now; + } if (time_zone_minutes < -1439 || time_zone_minutes > 1439) return SCE_RTC_ERROR_INVALID_VALUE; RtcTick local{}; if (const int result = addTicks(&local, utc, time_zone_minutes, TICKS_PER_MINUTE); result != 0) return result; @@ -570,7 +623,12 @@ int APS5_VABI sceRtcFormatRFC2822(char* date_time, const RtcTick* utc, int time_ } int APS5_VABI sceRtcFormatRFC2822LocalTime(char* date_time, const RtcTick* utc) { - if (!date_time || !utc) return SCE_RTC_ERROR_INVALID_POINTER; + if (!date_time) return SCE_RTC_ERROR_INVALID_POINTER; + RtcTick now{}; + if (!utc) { + now.tick = currentTick(); + utc = &now; + } if (utc->tick > MAX_TICK) return SCE_RTC_ERROR_INVALID_VALUE; return sceRtcFormatRFC2822(date_time, utc, localOffsetMinutes(*utc)); } @@ -595,31 +653,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 +685,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 +703,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 0dcf019ce6..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) { @@ -102,9 +141,12 @@ int APS5_VABI sceRudpEnableInternalIOThread(uint32_t stack_size, uint32_t priori return g_inited ? 0 : RUDP_ERROR_NOT_INITIALIZED; } -int APS5_VABI sceRudpEnableInternalIOThread2() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceRudpEnableInternalIOThread2(uint32_t stack_size, uint32_t priority, uint64_t affinity) { + (void)stack_size; + (void)priority; + (void)affinity; + std::lock_guard lk(g_mutex); + return g_inited ? 0 : RUDP_ERROR_NOT_INITIALIZED; } int APS5_VABI sceRudpSetEventHandler(GuestRudpEventHandler handler, void* arg) { @@ -156,7 +198,11 @@ int APS5_VABI sceRudpSetOption(int ctx_id, int option, const void* value, uint32 return valid_ctx(ctx_id) ? 0 : RUDP_ERROR_INVALID_CONTEXT_ID; } -int APS5_VABI sceRudpGetOption() { +int APS5_VABI sceRudpGetOption(int ctx_id, int option, void* value, uint32_t len) { + (void)ctx_id; + (void)option; + (void)value; + (void)len; NotImplemented_nid_no_patch(__func__); return 0; } @@ -270,8 +316,8 @@ int APS5_VABI sceRudpProcessEvents(void) { } int APS5_VABI sceRudpNetReceived(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard lk(g_mutex); + return g_inited ? 0 : RUDP_ERROR_NOT_INITIALIZED; } int APS5_VABI sceRudpGetMaxSegmentSize(uint16_t* size) { @@ -286,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 925cabeb13..a95498f7d9 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -18,6 +18,7 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/RemoveTree.hpp" #include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" #include "SaveData.hpp" #include "prx/libSceSaveData/SaveDataFile.hpp" @@ -340,7 +341,7 @@ static int deleteSave(const SaveDataDelete* del) { std::lock_guard lock(g_slots_mutex); if (isMountedDirectory(path)) return SAVE_DATA_ERROR_BUSY; if (std::filesystem::is_directory(path)) { - std::filesystem::remove_all(path); + RemoveTree(path); } std::error_code ec; std::filesystem::remove(param_path(path), ec); @@ -360,7 +361,7 @@ int APS5_VABI sceSaveDataDeleteTransactionResource(int32_t resource) { return SAVE_DATA_OK; } -static constexpr std::uint64_t SAVE_DATA_BLOCK_BYTES = 32768; +static constexpr std::uint64_t SAVE_DATA_BLOCK_BYTES = 65536; static std::uint64_t save_blocks(const std::string& real_path) { std::ifstream file(blocks_path(real_path)); @@ -590,7 +591,7 @@ int APS5_VABI sceSaveDataInitialize3(const void* init) { int APS5_VABI sceSaveDataLoadIcon(const SaveDataMountPoint* mount_point, SaveDataIcon* icon) { (void)icon; if (mount_point == nullptr) { - throw std::runtime_error("sceSaveDataLoadIcon: null mount_point"); + return SAVE_DATA_ERROR_PARAMETER; } std::lock_guard lock(g_slots_mutex); if (find_slot_by_mount_point(mount_point->data) == -1) { @@ -672,7 +673,7 @@ int APS5_VABI sceSaveDataPrepare(const SaveDataMountPoint* mount_point, const Sa int APS5_VABI sceSaveDataSaveIcon(const SaveDataMountPoint* mount_point, const SaveDataIcon* icon) { (void)icon; if (mount_point == nullptr) { - throw std::runtime_error("sceSaveDataSaveIcon: null mount_point"); + return SAVE_DATA_ERROR_PARAMETER; } std::lock_guard lock(g_slots_mutex); if (find_slot_by_mount_point(mount_point->data) == -1) { @@ -689,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); @@ -895,7 +896,7 @@ int APS5_VABI sceSaveDataTransferringMount(const SaveDataTransferringMount* moun static int umount2(uint32_t mode, const SaveDataMountPoint* mount_point) { (void)mode; if (mount_point == nullptr) { - throw std::runtime_error("sceSaveDataUmount2: null mount_point"); + return SAVE_DATA_ERROR_PARAMETER; } std::lock_guard lock(g_slots_mutex); int slot = find_slot_by_mount_point(mount_point->data); @@ -993,4 +994,40 @@ int APS5_VABI sceSaveDataPrepareForTransferring() { return 0; } +int APS5_VABI sceSaveDataCheckIpmiIfSize() { + return 0; +} + +int APS5_VABI sceSaveDataDebug() { + return 0; +} + +int APS5_VABI sceSaveDataDebugCheckBackupData() { + return 0; +} + +int APS5_VABI sceSaveDataDebugCleanMount() { + return 0; +} + +int APS5_VABI sceSaveDataDebugCompiledSdkVersion() { + return 0; +} + +int APS5_VABI sceSaveDataDebugCreateSaveDataRoot() { + return 0; +} + +int APS5_VABI sceSaveDataDebugProspero() { + return 0; +} + +int APS5_VABI sceSaveDataDebugRemoveSaveDataRoot() { + return 0; +} + +int APS5_VABI sceSaveDataDebugTarget() { + return 0; +} + } diff --git a/core/libs/prx/libSceSaveData.native/SaveData.hpp b/core/libs/prx/libSceSaveData.native/SaveData.hpp index ceffb09c01..1a8681cb38 100644 --- a/core/libs/prx/libSceSaveData.native/SaveData.hpp +++ b/core/libs/prx/libSceSaveData.native/SaveData.hpp @@ -25,7 +25,7 @@ constexpr std::uint32_t SAVE_DATA_MOUNT_MODE_RDWR = 2; constexpr std::uint32_t SAVE_DATA_MOUNT_MODE_CREATE = 4; constexpr std::uint32_t SAVE_DATA_MOUNT_MODE_CREATE2 = 32; -constexpr std::uint64_t SAVE_DATA_BLOCKS_MAX = 32768; +constexpr std::uint64_t SAVE_DATA_BLOCKS_MAX = 16384; constexpr std::uint32_t SAVE_DATA_PARAM_TYPE_ALL = 0; constexpr std::uint32_t SAVE_DATA_PARAM_TYPE_TITLE = 1; constexpr std::uint32_t SAVE_DATA_PARAM_TYPE_SUB_TITLE = 2; diff --git a/core/libs/prx/libSceSaveData/Export.cpp b/core/libs/prx/libSceSaveData/Export.cpp index 0d44da8d1b..f2a68ec989 100644 --- a/core/libs/prx/libSceSaveData/Export.cpp +++ b/core/libs/prx/libSceSaveData/Export.cpp @@ -91,4 +91,335 @@ int APS5_VABI sceSaveDataTransferringMount(const SaveDataTransferringMount* moun SAVEDATA_TRACE("sceSaveDataTransferringMount -> 0x%08x", static_cast(rc)); return rc; } + +int APS5_VABI sceSaveDataAbort() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataBindPsnAccount(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataBindPsnAccountForSystemBackup(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataChangeDatabase(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataChangeInternal(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataCheckBackupData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataCheckBackupDataForCdlg(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataCheckBackupDataInternal(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataCheckBackupDataLight(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataCheckCloudData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataCheckSaveDataBroken(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataCheckSaveDataVersion(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataCheckSaveDataVersionLatest(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataClearProgress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataCreateUploadData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataDebugGetThreadId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataDeleteAllUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataDeleteBackupData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataDeleteCloudData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataDeleteProspero(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataDeleteTitle(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataDeleteUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataDirNameSearchInternal(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataDownload(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataGetAllSize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataGetAppLaunchedUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataGetAutoUploadConditions(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataGetAutoUploadRequestInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataGetAutoUploadSetting(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataGetBoundPsnAccountCount(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataGetClientThreadPriority(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataGetCloudQuotaInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataGetEventInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataGetMountedSaveDataCount() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataGetProgress() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataGetSaveDataCount() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataGetSaveDataRootDir(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataGetSaveDataRootPath(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataGetSaveDataRootUsbPath(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataGetSavePoint(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataGetUpdatedDataCount(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataInitialize2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataInitializeForCdlg(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataIsDeletingUsbDb(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataIsMounted() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataManualDelete(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataManualDownload(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataManualUpload(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataMount5() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataMountInternal(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataMountSys(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataRebuildDatabase(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataRegisterEventCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataRestoreBackupData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataRestoreBackupDataForCdlg(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataRestoreLoadSaveDataMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataSetAutoUploadSetting(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataSetEventInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataSetSaveDataLibraryUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataShutdownStart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataSupportedFakeBrokenStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataSyncCloudList(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataUmountSys(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataUmountWithBackup(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataUnregisterEventCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSaveDataUpload(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceSaveData/SaveDataCore.hpp b/core/libs/prx/libSceSaveData/SaveDataCore.hpp index 4705d8d62e..58afdfa721 100644 --- a/core/libs/prx/libSceSaveData/SaveDataCore.hpp +++ b/core/libs/prx/libSceSaveData/SaveDataCore.hpp @@ -22,6 +22,7 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/RemoveTree.hpp" #include "prx/libSceSaveData/SaveDataFile.hpp" namespace savedata { @@ -498,14 +499,14 @@ int mount_internal(const SceSaveDataDirName* dir_name, std::uint32_t mount_mode, created = true; if (write_u64(save_blocks_path(directory), blocks) != SD_OK) { std::error_code cleanup; - std::filesystem::remove_all(directory, cleanup); + RemoveTree(directory, cleanup); return SD_ERROR_INTERNAL; } SaveDataParam initial {}; std::strncpy(initial.title, "Saved Data", sizeof(initial.title) - 1); if (!write_blob(save_param_path(directory), &initial, sizeof(initial))) { std::error_code cleanup; - std::filesystem::remove_all(directory, cleanup); + RemoveTree(directory, cleanup); return SD_ERROR_INTERNAL; } } @@ -574,7 +575,7 @@ int delete_internal(std::int32_t user_id, const SceSaveDataDirName* dir_name, if (!std::filesystem::exists(directory, error) || error) { return SD_ERROR_NOT_FOUND; } - std::filesystem::remove_all(directory, error); + RemoveTree(directory, error); RecordWrittenPath_nid_no_patch(directory); if (error) { return SD_ERROR_INTERNAL; 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/libSceSdma/Export.cpp b/core/libs/prx/libSceSdma/Export.cpp index d81f4a5e32..97a2ea23cc 100644 --- a/core/libs/prx/libSceSdma/Export.cpp +++ b/core/libs/prx/libSceSdma/Export.cpp @@ -6,5 +6,134 @@ extern "C" { APS5_DUMMY_FUN +int APS5_VABI sceSdmaBeginQueueMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdmaCancelQueue(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdmaClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdmaConstantFill(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdmaConstantFillNonBlocking(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdmaCopyLinear(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdmaCopyLinearNonBlocking(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdmaCopyTiled(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdmaCopyTiledNonBlocking(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdmaCopyWindowL2L(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdmaCopyWindowL2LNonBlocking(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdmaCopyWindowT2T(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdmaCopyWindowT2TNonBlocking(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdmaCopyWindowTiled(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdmaCopyWindowTiledNonBlocking(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdmaEndQueueMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdmaFenceNonBlocking(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdmaFinalize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdmaFlushAll(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdmaFlushAllWithEvent(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdmaInitialize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdmaKickQueue(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdmaOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdmaQueryWorkingBufferSize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdmaTryFlush(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSdmaWaitForEvent(void) { + 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..698fcbb800 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,18 +354,19 @@ 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); + auto* owner = static_cast(session.socketOwner.get()); + const auto sent = owner && owner->transfer + ? owner->transfer(owner, true, const_cast(buffer), chunk) + : ::send(static_cast(session.socket), reinterpret_cast(buffer), chunk, 0); #else const auto sent = ::send(static_cast(session.socket), buffer, static_cast(chunk), MSG_NOSIGNAL); #endif 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,51 +376,61 @@ 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); + auto* owner = static_cast(session.socketOwner.get()); + const auto received = owner && owner->transfer + ? owner->transfer(owner, false, buffer, chunk) + : ::recv(static_cast(session.socket), reinterpret_cast(buffer), chunk, 0); #else const auto received = ::recv(static_cast(session.socket), buffer, static_cast(chunk), 0); #endif 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 +445,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 +455,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 +482,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 +569,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 +591,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 +614,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 +627,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 +784,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 +798,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 +873,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 +959,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 +973,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 +1004,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 +1020,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 +1051,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/libSceSulphaDrv/Export.cpp b/core/libs/prx/libSceSulphaDrv/Export.cpp index d81f4a5e32..b956051655 100644 --- a/core/libs/prx/libSceSulphaDrv/Export.cpp +++ b/core/libs/prx/libSceSulphaDrv/Export.cpp @@ -6,5 +6,44 @@ extern "C" { APS5_DUMMY_FUN +int APS5_VABI sceSulphaGetClientInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSulphaGetConnectedClientCount(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSulphaGetDefaultConfig(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSulphaGetNeededMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSulphaInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSulphaMessage(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSulphaSetBookmark(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSulphaShutdown(void) { + NotImplemented_nid_no_patch(__func__); + 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 22aad137a6..4d74061f67 100644 --- a/core/libs/prx/libSceSystemGesture/Export.cpp +++ b/core/libs/prx/libSceSystemGesture/Export.cpp @@ -2,11 +2,13 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include #include +#include #include #include +#include -// No touch input is emulated: recognizers exist but never report events. static constexpr int32_t GESTURE_HANDLE = 1; static constexpr int SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT = static_cast(0x80D10002); static constexpr int SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE = static_cast(0x80D10003); @@ -34,6 +36,98 @@ bool loadRecognizerState(const SystemGestureTouchRecognizer* recognizer, Recogni return state.magic == RECOGNIZER_STATE_MAGIC; } +constexpr std::int32_t TOUCH_STATE_BEGIN = 1; +constexpr std::int32_t TOUCH_STATE_ACTIVE = 2; +constexpr std::int32_t TOUCH_STATE_END = 3; + +struct TrackedTouch { + std::uint16_t primitiveId; + std::int32_t eventState; + bool isUpdated; + bool sampled; + SystemGestureVector2 pressedPosition; + SystemGestureVector2 currentPosition; + SystemGestureVector2 deltaVector; + std::uint64_t pressedTime; + std::uint64_t updatedTime; + std::uint64_t deltaTime; +}; + +std::mutex primitiveTouchMutex; +std::vector primitiveTouches; + +struct TouchSample { + std::uint16_t primitiveId; + SystemGestureVector2 position; +}; + +std::vector reportSamples(const PadData& report) { + std::vector samples; + const std::uint8_t count = report.touch_data_touch_num; + if (count > 0) + samples.push_back(TouchSample{report.touch_data_touch0_id, + SystemGestureVector2{static_cast(report.touch_data_touch0_x), static_cast(report.touch_data_touch0_y)}}); + if (count > 1) + samples.push_back(TouchSample{report.touch_data_touch1_id, + SystemGestureVector2{static_cast(report.touch_data_touch1_x), static_cast(report.touch_data_touch1_y)}}); + return samples; +} + +void sampleReport(const PadData& report) { + std::erase_if(primitiveTouches, [](const TrackedTouch& touch) { return touch.eventState == TOUCH_STATE_END; }); + for (TrackedTouch& touch : primitiveTouches) { + touch.sampled = false; + touch.isUpdated = false; + } + const std::uint64_t time = report.timestamp; + for (const TouchSample& sample : reportSamples(report)) { + const auto tracked = std::find_if(primitiveTouches.begin(), primitiveTouches.end(), + [&](const TrackedTouch& touch) { return touch.primitiveId == sample.primitiveId; }); + if (tracked == primitiveTouches.end()) { + TrackedTouch touch{}; + touch.primitiveId = sample.primitiveId; + touch.eventState = TOUCH_STATE_BEGIN; + touch.isUpdated = true; + touch.sampled = true; + touch.pressedPosition = sample.position; + touch.currentPosition = sample.position; + touch.pressedTime = time; + touch.updatedTime = time; + primitiveTouches.push_back(touch); + continue; + } + const SystemGestureVector2 previous = tracked->currentPosition; + tracked->deltaVector = SystemGestureVector2{sample.position.x - previous.x, sample.position.y - previous.y}; + tracked->currentPosition = sample.position; + tracked->deltaTime = time >= tracked->updatedTime ? time - tracked->updatedTime : 0; + tracked->isUpdated = tracked->eventState != TOUCH_STATE_ACTIVE || tracked->deltaVector.x != 0.0f || tracked->deltaVector.y != 0.0f; + tracked->eventState = TOUCH_STATE_ACTIVE; + tracked->sampled = true; + tracked->updatedTime = time; + } + for (TrackedTouch& touch : primitiveTouches) { + if (touch.sampled) continue; + touch.deltaVector = SystemGestureVector2{0.0f, 0.0f}; + touch.deltaTime = time >= touch.updatedTime ? time - touch.updatedTime : 0; + touch.eventState = TOUCH_STATE_END; + touch.isUpdated = true; + touch.updatedTime = time; + } +} + +SystemGesturePrimitiveTouchEvent buildPrimitiveTouchEvent(const TrackedTouch& touch) { + SystemGesturePrimitiveTouchEvent event{}; + event.event_state = touch.eventState; + event.primitive_id = touch.primitiveId; + event.is_updated = touch.isUpdated ? 1 : 0; + event.pressed_position = touch.pressedPosition; + event.current_position = touch.currentPosition; + event.delta_vector = touch.deltaVector; + event.delta_time = touch.deltaTime; + event.elapsed_time = touch.updatedTime - touch.pressedTime; + return event; +} + } extern "C" { @@ -62,44 +156,55 @@ int APS5_VABI sceSystemGestureCreateTouchRecognizer(int32_t gesture_handle, Syst } int APS5_VABI sceSystemGestureFinalizePrimitiveTouchRecognizer(void) { + std::lock_guard lock(primitiveTouchMutex); + primitiveTouches.clear(); return 0; } int APS5_VABI sceSystemGestureGetPrimitiveTouchEventByIndex(int32_t gesture_handle, uint32_t index, SystemGesturePrimitiveTouchEvent* event) { - (void)index; - (void)event; if (gesture_handle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; - return SCE_SYSTEM_GESTURE_ERROR_INDEX_OUT_OF_ARRAY; + std::lock_guard lock(primitiveTouchMutex); + if (index >= primitiveTouches.size()) return SCE_SYSTEM_GESTURE_ERROR_INDEX_OUT_OF_ARRAY; + if (!event) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; + *event = buildPrimitiveTouchEvent(primitiveTouches[index]); + return 0; } int APS5_VABI sceSystemGestureGetPrimitiveTouchEventByPrimitiveID(int32_t gesture_handle, uint16_t primitiveId, SystemGesturePrimitiveTouchEvent* event) { - (void)gesture_handle; - (void)primitiveId; - (void)event; - NotImplemented_nid_no_patch(__func__); - return 0; + if (gesture_handle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; + if (!event) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(primitiveTouchMutex); + const auto tracked = std::find_if(primitiveTouches.begin(), primitiveTouches.end(), + [&](const TrackedTouch& touch) { return touch.primitiveId == primitiveId; }); + if (tracked == primitiveTouches.end()) + throw std::runtime_error(std::string(__func__) + ": no primitive touch event with id " + std::to_string(primitiveId)); + *event = buildPrimitiveTouchEvent(*tracked); + return 0; } int APS5_VABI sceSystemGestureGetPrimitiveTouchEvents(int32_t gesture_handle, SystemGesturePrimitiveTouchEvent* event_buffer, uint32_t capacity_of_buffer, uint32_t* number_of_event) { - (void)event_buffer; - (void)capacity_of_buffer; if (gesture_handle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; if (!number_of_event) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; - *number_of_event = 0; + std::lock_guard lock(primitiveTouchMutex); + const std::uint32_t reported = static_cast(std::min(primitiveTouches.size(), capacity_of_buffer)); + if (reported > 0 && !event_buffer) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; + for (std::uint32_t i = 0; i < reported; ++i) event_buffer[i] = buildPrimitiveTouchEvent(primitiveTouches[i]); + *number_of_event = reported; return 0; } int APS5_VABI sceSystemGestureGetPrimitiveTouchEventsCount(int32_t gesture_handle) { - return gesture_handle == GESTURE_HANDLE ? 0 : SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; + if (gesture_handle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; + std::lock_guard lock(primitiveTouchMutex); + return static_cast(primitiveTouches.size()); } 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) { @@ -141,6 +246,8 @@ int APS5_VABI sceSystemGestureGetTouchRecognizerInformation(int32_t gesture_hand int APS5_VABI sceSystemGestureInitializePrimitiveTouchRecognizer(const void* param) { (void)param; + std::lock_guard lock(primitiveTouchMutex); + primitiveTouches.clear(); return 0; } @@ -158,6 +265,8 @@ int APS5_VABI sceSystemGestureRemoveTouchRecognizer(int32_t gesture_handle, Syst int APS5_VABI sceSystemGestureResetPrimitiveTouchRecognizer(int32_t gesture_handle) { if (gesture_handle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; + std::lock_guard lock(primitiveTouchMutex); + primitiveTouches.clear(); return 0; } @@ -173,8 +282,12 @@ int APS5_VABI sceSystemGestureUpdateAllTouchRecognizer(int32_t gesture_handle) { } int APS5_VABI sceSystemGestureUpdatePrimitiveTouchRecognizer(int32_t gesture_handle, const void* param) { - (void)param; if (gesture_handle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; + if (!param) return 0; + const auto* input = static_cast(param); + if (!input->pad_data_buffer || input->report_number <= 0) return 0; + std::lock_guard lock(primitiveTouchMutex); + for (std::int32_t i = 0; i < input->report_number; ++i) sampleReport(input->pad_data_buffer[i]); return 0; } @@ -197,4 +310,8 @@ int APS5_VABI sceSystemGestureUpdateTouchRecognizerRectangle(int32_t gesture_han return 0; } +int APS5_VABI sceSystemGestureDebugGetVersion() { + return 0x6a00; +} + } diff --git a/core/libs/prx/libSceSystemService/CMakeLists.txt b/core/libs/prx/libSceSystemService/CMakeLists.txt index 8c533a1553..3542172d1b 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) @@ -38,6 +38,8 @@ if(BUILD_TESTING) add_test(NAME guest_requested_exit COMMAND guest_system_exit_tests --requested-exit-driver) add_test(NAME guest_quick_exit COMMAND guest_system_exit_tests --quick-exit) add_test(NAME guest_immediate_exit COMMAND guest_system_exit_tests --immediate) + add_test(NAME guest_immediate_exit_drain COMMAND guest_system_exit_tests --exit-drain-driver) + set_tests_properties(guest_immediate_exit_drain PROPERTIES TIMEOUT 20) add_test(NAME guest_return_from_main COMMAND guest_system_exit_tests --return-from-main) add_test(NAME guest_system_relaunch COMMAND guest_system_exit_tests --relaunch-driver) add_test(NAME guest_nested_exit COMMAND guest_system_exit_tests --nested-exit-driver) @@ -62,7 +64,7 @@ if(BUILD_TESTING) set_tests_properties(guest_system_load_exec_standard_error PROPERTIES TIMEOUT 20) endif() if(WIN32) - set_tests_properties(guest_system_exit guest_requested_exit guest_system_exit_parameters guest_immediate_exit guest_return_from_main guest_quick_exit guest_system_relaunch guest_nested_exit guest_exit_finalizers_before_shutdown PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_system_exit guest_requested_exit guest_system_exit_parameters guest_immediate_exit guest_immediate_exit_drain guest_return_from_main guest_quick_exit guest_system_relaunch guest_nested_exit guest_exit_finalizers_before_shutdown PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test_executable(guest_system_service_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemService.cpp) @@ -99,10 +101,14 @@ if(BUILD_TESTING) add_test(NAME guest_system_kill_app_parameters COMMAND guest_system_kill_app_tests) add_test(NAME guest_system_kill_app COMMAND guest_system_kill_app_tests --kill) add_test(NAME guest_system_kill_app_status COMMAND guest_system_kill_app_tests --kill) - set_tests_properties(guest_system_kill_app PROPERTIES + add_test(NAME guest_lnc_util_kill_app COMMAND guest_system_kill_app_tests --lnc-kill) + add_test(NAME guest_lnc_util_kill_app_with_reason COMMAND guest_system_kill_app_tests --lnc-kill-reason) + add_test(NAME guest_lnc_util_kill_app_status COMMAND guest_system_kill_app_tests --lnc-kill) + add_test(NAME guest_lnc_util_kill_app_with_reason_status COMMAND guest_system_kill_app_tests --lnc-kill-reason) + set_tests_properties(guest_system_kill_app guest_lnc_util_kill_app guest_lnc_util_kill_app_with_reason PROPERTIES PASS_REGULAR_EXPRESSION "Guest shutdown and atexit completed") if(WIN32) - set_tests_properties(guest_system_kill_app guest_system_kill_app_status guest_system_kill_app_parameters PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_system_kill_app guest_system_kill_app_status guest_system_kill_app_parameters guest_lnc_util_kill_app guest_lnc_util_kill_app_with_reason guest_lnc_util_kill_app_status guest_lnc_util_kill_app_with_reason_status PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceSystemService/Export.cpp b/core/libs/prx/libSceSystemService/Export.cpp index 60cda5b360..6c37c6863d 100644 --- a/core/libs/prx/libSceSystemService/Export.cpp +++ b/core/libs/prx/libSceSystemService/Export.cpp @@ -262,6 +262,10 @@ int APS5_VABI sceSystemServiceGetDisplaySafeAreaInfo(SystemServiceDisplaySafeAre return SYSTEM_SERVICE_OK; } +int APS5_VABI sceSystemServiceShowDisplaySafeAreaSettings(void) { + return SYSTEM_SERVICE_OK; +} + int APS5_VABI sceSystemServiceGetHdrToneMapLuminance(SystemServiceHdrToneMapLuminance* luminance) { if (luminance == nullptr) { return SYSTEM_SERVICE_ERROR_PARAMETER; @@ -313,16 +317,11 @@ int APS5_VABI sceSystemServiceParamGetInt(int paramId, int* value) { } int APS5_VABI sceSystemServiceParamGetString(int paramId, char* buf, size_t bufSize) { - if (buf == nullptr || bufSize == 0) { + if (paramId != SYSTEM_SERVICE_PARAM_ID_SYSTEM_NAME || buf == nullptr || bufSize < SYSTEM_SERVICE_MAX_SYSTEM_NAME_LENGTH) { return SYSTEM_SERVICE_ERROR_PARAMETER; } - if (paramId != SYSTEM_SERVICE_PARAM_ID_SYSTEM_NAME) { - NotImplemented_nid_no_patch("sceSystemServiceParamGetString: parameter other than the system name"); - } - if (bufSize < SYSTEM_SERVICE_MAX_SYSTEM_NAME_LENGTH) { - NotImplemented_nid_no_patch("sceSystemServiceParamGetString: buffer shorter than 65 bytes"); - } constexpr char SystemName[] = "PS5"; + std::memset(buf, 0, bufSize); std::memcpy(buf, SystemName, sizeof(SystemName)); return SYSTEM_SERVICE_OK; } @@ -347,9 +346,11 @@ int APS5_VABI sceSystemServiceSetNoticeScreenSkipFlag(void) { return SYSTEM_SERVICE_OK; } -int APS5_VABI sceSystemServiceInitializePlayerDialogParam(void* param) { - if (param == nullptr) return SYSTEM_SERVICE_ERROR_PARAMETER; - return SYSTEM_SERVICE_OK; +void APS5_VABI sceSystemServiceInitializePlayerDialogParam(SystemServicePlayerDialogParam* param) { + if (param == nullptr) APS5_INVALID_ARG_EX; + *param = SystemServicePlayerDialogParam{}; + param->size = sizeof(SystemServicePlayerDialogParam); + param->user_id = -1; } int APS5_VABI sceSystemServiceDisableMediaPlay() { @@ -388,6 +389,15 @@ int APS5_VABI sceSystemServiceOpenChallengeActivity(void) { } int APS5_VABI sceSystemServiceOpenTournamentOccurrence(void) { + return SYSTEM_SERVICE_OK; +} + +int APS5_VABI sceSystemServiceAddLocalProcessForPsmKit(const char*, const char* const*, const void*) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceKillLocalProcessForPsmKit(int) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -414,4 +424,411 @@ int APS5_VABI sceSystemServiceKillApp(int appId, int how, int reason, int coreDu LibcExit_nid_no_patch(0); } +int APS5_VABI sceLncUtilKillApp(uint32_t appId) { + if (appId != static_cast(SYSTEM_SERVICE_RUNNING_APP_ID)) { + NotImplemented_nid_no_patch("sceLncUtilKillApp: application other than the running title"); + } + LibcExit_nid_no_patch(0); +} + +int APS5_VABI sceLncUtilKillAppWithReason(uint32_t appId, int reason) { + if (appId != static_cast(SYSTEM_SERVICE_RUNNING_APP_ID)) { + NotImplemented_nid_no_patch("sceLncUtilKillAppWithReason: application other than the running title"); + } + if (reason != 0) { + NotImplemented_nid_no_patch("sceLncUtilKillAppWithReason: a reason other than 0"); + } + LibcExit_nid_no_patch(0); +} + +int APS5_VABI sceSystemServiceLaunchSystemDeeplink(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilLaunchByUriParamInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrTickVideoPlayback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceTelemetrySetData2WithUserId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilStartUmmDictation(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetSpotifyTitleId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceGetAppStatusListContainsDaemon(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceSwitchToVRApp(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceSwitchToNonVRApp(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetSpmemAllocatorConfigValue(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCsbMgrInitialize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCsbMgrTerminate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCsbMgrCreateLocalSandbox(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCsbMgrDestroyLocalSandbox(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCsbMgrSwitchLocalSandbox(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCsbMgrGetCurrentLocalSandboxId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCsbMgrGetLocalSandboxIdList(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCsbMgrGetLocalSandboxInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCsbMgrSetLocalSandboxInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCsbMgrIssueStreamingSandboxInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceReportSequenceHandlerRequestReport(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceReportSequenceHandlerCancelReport(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceReportSequenceHandlerExistsEvent(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceReportSequenceHandlerUpdateEventCache(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceReportSequenceHandlerGetEventCache(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilRegisterCallEntries(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilGetSpmemConfigJson(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilExportSpmemConfigJson(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLncUtilImportSpmemConfigJson(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilExitAppWithValue(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilNpDebugSpecialNotification(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsDddTemperatureDanger(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsM2StorageTemperatureDanger(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetAvailableSizeM2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilNotifyConnectAccountStatusChanged(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsInternalCronosUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilChangePbtcMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilTelemetryCacheData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilMicMuteKeyPressed(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSwitchMicMute(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilNotifyStartEventToUniversalMicMute(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsLotusMicActivated(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilResetAllNoticeScreenSkipFlag(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilResetNoticeScreenSkipFlag(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetNoticeScreenSkipFlagByAppId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetPasscodeInputType(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetPasscodeDisplayInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilNotifySystemResumeTelemetryInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilUpdateTelemetryToken(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetVRBaseMatRectangles(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilSetVRBaseMatRectangles(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsLaunchBlockDialog(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetConsoleToken(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetDeviceIdByDeviceUniqueId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetDeviceUniqueIdByDeviceId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetStreamingClientInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilRequestCffUpdate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetCffConfig(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilRemoveCffConfigFile(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilDebugGetNumberOfCffItems(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilDebugGetCffInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilDebugSetCffStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetPeripheralConnectionSessionId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetCdlgEnabledFreezeRendering(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilGetNpTitleId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsCronosInGameCommerceEnabled(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilLemSettingGet(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilLemSettingSet(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilLemSettingGetList(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilLemSettingReset(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShellCoreUtilIsEChassisVariant(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemStateMgrGetRebootCause(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCoredumpSequenceHandlerUpdateEventCache(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCoredumpSequenceHandlerGetEventCache(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCoredumpSequenceHandlerGetWorkingEventSummary(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCoredumpSequenceHandlerGetCoredumpStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCoredumpSequenceHandlerGetCoredumpStatusByAppId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCoredumpSequenceHandlerCancelCoredump(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceSystemService/PendingExports.cpp b/core/libs/prx/libSceSystemService/PendingExports.cpp new file mode 100644 index 0000000000..9ac7176e02 --- /dev/null +++ b/core/libs/prx/libSceSystemService/PendingExports.cpp @@ -0,0 +1,2361 @@ +#include "prx/libc/include/General.hpp" + +namespace { + +constexpr int SystemServiceUnavailable = static_cast(0x80A10002); +constexpr int SystemServiceRejected = static_cast(0x80A10005); + +} + +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 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() { + 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() { + return 0; +} + +int APS5_VABI sceShellCoreUtilActivateStart() { + return 0; +} + +int APS5_VABI sceShellCoreUtilActivateStartAsync() { + return 0; +} + +int APS5_VABI sceShellCoreUtilActivateTerm() { + 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() { + return SystemServiceUnavailable; +} + +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() { + return 0; +} + +int APS5_VABI sceSystemServiceChangeClock() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceChangeCpuClock() { + return 0; +} + +int APS5_VABI sceSystemServiceChangeCpuPstate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceChangeGpuClock() { + return 0; +} + +int APS5_VABI sceSystemServiceChangeMemPstate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceChangeMemoryClock() { + return 0; +} + +int APS5_VABI sceSystemServiceChangeMemoryClockToBaseMode() { + return 0; +} + +int APS5_VABI sceSystemServiceChangeMemoryClockToDefault() { + return 0; +} + +int APS5_VABI sceSystemServiceChangeMemoryClockToMultiMediaMode() { + return 0; +} + +int APS5_VABI sceSystemServiceChangeNumberOfGpuCu() { + return 0; +} + +int APS5_VABI sceSystemServiceChangeSamuClock() { + return 0; +} + +int APS5_VABI sceSystemServiceChangeUvdClock() { + return 0; +} + +int APS5_VABI sceSystemServiceChangeVceClock() { + return 0; +} + +int APS5_VABI sceSystemServiceDeclareReadyForSuspend() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceDisablePartyVoice() { + return SystemServiceUnavailable; +} + +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() { + return SystemServiceUnavailable; +} + +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() { + return SystemServiceRejected; +} + +int APS5_VABI sceSystemServiceLaunchWebApp() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceLoadExecVideoServiceWebApp() { + return SystemServiceUnavailable; +} + +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() { + return SystemServiceUnavailable; +} + +int APS5_VABI sceSystemServiceReenableVoiceRecognition() { + return SystemServiceUnavailable; +} + +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() { + return SystemServiceUnavailable; +} + +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() { + return SystemServiceRejected; +} + +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() { + 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/libSceTEEClient/Export.cpp b/core/libs/prx/libSceTEEClient/Export.cpp index d81f4a5e32..58c703695b 100644 --- a/core/libs/prx/libSceTEEClient/Export.cpp +++ b/core/libs/prx/libSceTEEClient/Export.cpp @@ -6,5 +6,54 @@ extern "C" { APS5_DUMMY_FUN +int APS5_VABI AMDTEE_DLM_FetchDebugStrings_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI AMDTEE_DLM_GetDebugToken_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI AMDTEE_DLM_StopTADebug_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI TEECI_InitASD_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI TEECI_SetTimeout_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI TEEC_CloseSession_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI TEEC_InvokeCommand_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI TEEC_OpenSession_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI TEEC_ReleaseSharedMemory_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI TEEC_RequestCancellation_nid_postfix(void) { + 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..ea7e2f4a31 100644 --- a/core/libs/prx/libSceUlt/CMakeLists.txt +++ b/core/libs/prx/libSceUlt/CMakeLists.txt @@ -1,8 +1,21 @@ -add_library(libSceUlt SHARED EXCLUDE_FROM_ALL Export.cpp) +add_library(libSceUlt SHARED EXCLUDE_FROM_ALL Export.cpp src/ReaderWriterLock.cpp) 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) @@ -28,4 +41,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endforeach() + add_test_executable(guest_ult_sync_tests ${LIBS_INCLUDE_DIR}/tests/GuestUltSync.cpp) + target_include_directories(guest_ult_sync_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_ult_sync_tests PRIVATE libSceUlt libc) + configure_windows_unwind(guest_ult_sync_tests) + foreach(testCase IN ITEMS mutex-trylock opt-params rwlock-validation rwlock-readers rwlock-writer rwlock-pool rwlock-finalize ulthreads) + add_test(NAME guest_ult_sync_${testCase} COMMAND guest_ult_sync_tests ${testCase}) + set_tests_properties(guest_ult_sync_${testCase} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_ult_sync_${testCase} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() endif() diff --git a/core/libs/prx/libSceUlt/Export.cpp b/core/libs/prx/libSceUlt/Export.cpp index 506333c24e..10ba814b81 100644 --- a/core/libs/prx/libSceUlt/Export.cpp +++ b/core/libs/prx/libSceUlt/Export.cpp @@ -14,15 +14,19 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "SceUltTypes.hpp" +#include "prx/libSceUlt/src/ReaderWriterLock.hpp" extern "C" int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); extern "C" int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +extern "C" void APS5_VABI scePthreadExit(void* retval); +extern "C" void APS5_VABI scePthreadYield(); namespace { std::mutex gMutex; std::unordered_map> gMutexes; +std::unordered_map> gConditions; std::unordered_map> gSemaphores; std::unordered_map gResourcePools; std::unordered_map> gQueueDataPools; @@ -107,8 +111,11 @@ void ReleaseQueue(UltQueueState& state) { --pool._queues; } +thread_local UltUlthreadState* gCurrentUlthread = nullptr; + void* APS5_VABI ulthreadRunner(void* arg) { auto* state = static_cast(arg); + gCurrentUlthread = state; const auto result = state->_entry(state->_arg); state->_exited.store(true, std::memory_order_release); return reinterpret_cast(static_cast(result)); @@ -116,6 +123,11 @@ void* APS5_VABI ulthreadRunner(void* arg) { } +bool UltWaitingPoolExists(void* pool) { + std::lock_guard lock(gMutex); + return gResourcePools.contains(pool); +} + extern "C" { int APS5_VABI sceUltInitialize() { @@ -145,7 +157,15 @@ 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(); + UltFinalizeReaderWriterLocks(); gResourcePools.clear(); for (auto& entry : gQueues) { auto& state = *entry.second; @@ -176,6 +196,31 @@ int APS5_VABI sceUltMutexOptParamInitialize(UltMutexOptParam* optParam, std::uin return ULT_OK; } +int APS5_VABI sceUltSemaphoreOptParamInitialize(UltOptParam* optParam, std::uint32_t buildVersion) { + (void)buildVersion; + return InitializeOptParam(optParam); +} + +int APS5_VABI sceUltQueueOptParamInitialize(UltOptParam* optParam, std::uint32_t buildVersion) { + (void)buildVersion; + return InitializeOptParam(optParam); +} + +int APS5_VABI sceUltQueueDataResourcePoolOptParamInitialize(UltOptParam* optParam, std::uint32_t buildVersion) { + (void)buildVersion; + return InitializeOptParam(optParam); +} + +int APS5_VABI sceUltWaitingQueueResourcePoolOptParamInitialize(UltOptParam* optParam, std::uint32_t buildVersion) { + (void)buildVersion; + return InitializeOptParam(optParam); +} + +int APS5_VABI sceUltUlthreadOptParamInitialize(UltOptParam* optParam, std::uint32_t buildVersion) { + (void)buildVersion; + return InitializeOptParam(optParam); +} + int APS5_VABI sceUltMutexCreate(void* mutex, const char* name, void* waitingQueueResourcePool, const UltMutexOptParam* optParam, std::uint32_t buildVersion) { (void)name; (void)buildVersion; @@ -208,7 +253,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; @@ -253,6 +298,33 @@ int APS5_VABI sceUltMutexLock(void* mutex) { return ULT_OK; } +int APS5_VABI sceUltMutexTryLock(void* mutex) { + std::shared_ptr state; + { + std::lock_guard lock(gMutex); + auto it = gMutexes.find(mutex); + if (it == gMutexes.end()) { + return mutex == nullptr ? ULT_ERROR_NULL : ULT_ERROR_STATE; + } + state = it->second; + } + const auto self = std::this_thread::get_id(); + std::lock_guard lock(state->_mutex); + if (!state->_alive) { + return ULT_ERROR_STATE; + } + if (state->_owner == self) { + ++state->_lockCount; + return ULT_OK; + } + if (state->_lockCount != 0) { + return ULT_ERROR_BUSY; + } + state->_owner = self; + state->_lockCount = 1; + return ULT_OK; +} + int APS5_VABI sceUltMutexUnlock(void* mutex) { std::shared_ptr state; { @@ -276,6 +348,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)); } @@ -326,6 +493,9 @@ int APS5_VABI sceUltWaitingQueueResourcePoolDestroy(void* pool) { } } } + if (UltReaderWriterLocksWaitOn(pool)) { + return ULT_ERROR_BUSY; + } gResourcePools.erase(it); return ULT_OK; } @@ -596,6 +766,7 @@ int APS5_VABI sceUltUlthreadCreate(void* ulthread, const char* name, UltUlthread auto state = std::make_shared(); state->_entry = entry; state->_arg = arg; + state->_object = ulthread; state->_runtime = runtime; { std::lock_guard lock(gMutex); @@ -659,6 +830,34 @@ int APS5_VABI sceUltUlthreadTryJoin(void* ulthread, std::int32_t* status) { return sceUltUlthreadJoin(ulthread, status); } +int APS5_VABI sceUltUlthreadGetSelf(void** ulthread) { + if (ulthread == nullptr) { + return ULT_ERROR_NULL; + } + if (gCurrentUlthread == nullptr) { + return ULT_ERROR_STATE; + } + *ulthread = gCurrentUlthread->_object; + return ULT_OK; +} + +int APS5_VABI sceUltUlthreadYield() { + if (gCurrentUlthread == nullptr) { + return ULT_ERROR_STATE; + } + scePthreadYield(); + return ULT_OK; +} + +int APS5_VABI sceUltUlthreadExit(std::int32_t status) { + if (gCurrentUlthread == nullptr) { + return ULT_ERROR_STATE; + } + gCurrentUlthread->_exited.store(true, std::memory_order_release); + scePthreadExit(reinterpret_cast(static_cast(status))); + return ULT_OK; +} + int APS5_VABI sceUltUlthreadRuntimeDestroy(void* runtime) { if (runtime == nullptr) { return ULT_ERROR_NULL; @@ -713,4 +912,130 @@ 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 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 _sceUltConditionVariableOptParamInitialize() { + 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..efe53ec5a8 100644 --- a/core/libs/prx/libSceUlt/SceUltTypes.hpp +++ b/core/libs/prx/libSceUlt/SceUltTypes.hpp @@ -4,7 +4,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -19,17 +21,43 @@ constexpr int ULT_ERROR_STATE = -2139029498; constexpr int ULT_ERROR_BUSY = -2139029497; constexpr int ULT_ERROR_AGAIN = -2139029496; +struct UltOptParam { + std::uint8_t bytes[128]; +}; + +inline int InitializeOptParam(UltOptParam* optParam) { + if (optParam == nullptr) { + return ULT_ERROR_NULL; + } + std::memset(optParam, 0, sizeof(*optParam)); + return ULT_OK; +} + struct UltMutexState { std::mutex _mutex; std::condition_variable _available; 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; @@ -78,6 +106,7 @@ struct UltRuntimeState { struct UltUlthreadState { UltUlthreadEntry _entry = nullptr; std::uint64_t _arg = 0; + void* _object = nullptr; Pthread _thread = nullptr; void* _runtime = nullptr; std::atomic _exited{false}; diff --git a/core/libs/prx/libSceUlt/src/ReaderWriterLock.cpp b/core/libs/prx/libSceUlt/src/ReaderWriterLock.cpp new file mode 100644 index 0000000000..7e1273394a --- /dev/null +++ b/core/libs/prx/libSceUlt/src/ReaderWriterLock.cpp @@ -0,0 +1,200 @@ +#include "prx/libSceUlt/src/ReaderWriterLock.hpp" +#include "prx/libSceUlt/SceUltTypes.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +struct UltReaderWriterLockState { + std::mutex _mutex; + std::condition_variable _released; + std::uint32_t _readers = 0; + bool _writing = false; + std::thread::id _writer{}; + std::uint32_t _waiters = 0; + bool _alive = true; + void* _waitingPool = nullptr; +}; + +std::mutex gLocksMutex; +std::unordered_map> gLocks; + +int FindLock(void* rwlock, std::shared_ptr& state) { + if (rwlock == nullptr) { + return ULT_ERROR_NULL; + } + std::lock_guard lock(gLocksMutex); + const auto it = gLocks.find(rwlock); + if (it == gLocks.end()) { + return ULT_ERROR_STATE; + } + state = it->second; + return ULT_OK; +} + +bool HeldByCaller(const UltReaderWriterLockState& state) { + return state._writing && state._writer == std::this_thread::get_id(); +} + +int Wait(UltReaderWriterLockState& state, std::unique_lock& lock, bool writer) { + ++state._waiters; + state._released.wait(lock, [&] { return !state._alive || (!state._writing && (!writer || state._readers == 0)); }); + --state._waiters; + return state._alive ? ULT_OK : ULT_ERROR_STATE; +} + +} + +bool UltReaderWriterLocksWaitOn(void* pool) { + std::lock_guard lock(gLocksMutex); + for (const auto& entry : gLocks) { + if (entry.second->_waitingPool != pool) continue; + std::lock_guard stateLock(entry.second->_mutex); + if (entry.second->_waiters != 0) return true; + } + return false; +} + +void UltFinalizeReaderWriterLocks() { + std::lock_guard lock(gLocksMutex); + for (const auto& entry : gLocks) { + std::lock_guard stateLock(entry.second->_mutex); + entry.second->_alive = false; + entry.second->_released.notify_all(); + } + gLocks.clear(); +} + +extern "C" { + +int APS5_VABI sceUltReaderWriterLockOptParamInitialize(UltOptParam* optParam, std::uint32_t buildVersion) { + (void)buildVersion; + return InitializeOptParam(optParam); +} + +int APS5_VABI sceUltReaderWriterLockCreate(void* rwlock, const char* name, void* waitingQueueResourcePool, const UltOptParam* optParam, std::uint32_t buildVersion) { + (void)name; + (void)optParam; + (void)buildVersion; + if (rwlock == nullptr) { + return ULT_ERROR_NULL; + } + if (waitingQueueResourcePool != nullptr && !UltWaitingPoolExists(waitingQueueResourcePool)) { + return ULT_ERROR_INVALID; + } + auto state = std::make_shared(); + state->_waitingPool = waitingQueueResourcePool; + std::lock_guard lock(gLocksMutex); + if (gLocks.contains(rwlock)) { + return ULT_ERROR_STATE; + } + std::memset(rwlock, 0, 256); + gLocks[rwlock] = std::move(state); + return ULT_OK; +} + +int APS5_VABI sceUltReaderWriterLockDestroy(void* rwlock) { + if (rwlock == nullptr) { + return ULT_ERROR_NULL; + } + std::lock_guard lock(gLocksMutex); + const auto it = gLocks.find(rwlock); + if (it == gLocks.end()) { + return ULT_ERROR_STATE; + } + auto& state = *it->second; + std::lock_guard stateLock(state._mutex); + if (state._readers != 0 || state._writing || state._waiters != 0) { + return ULT_ERROR_BUSY; + } + state._alive = false; + gLocks.erase(it); + return ULT_OK; +} + +int APS5_VABI sceUltReaderWriterLockLockRead(void* rwlock) { + std::shared_ptr state; + if (const int found = FindLock(rwlock, state); found != ULT_OK) return found; + std::unique_lock lock(state->_mutex); + if (!state->_alive || HeldByCaller(*state)) { + return ULT_ERROR_STATE; + } + if (const int waited = Wait(*state, lock, false); waited != ULT_OK) return waited; + ++state->_readers; + return ULT_OK; +} + +int APS5_VABI sceUltReaderWriterLockTryLockRead(void* rwlock) { + std::shared_ptr state; + if (const int found = FindLock(rwlock, state); found != ULT_OK) return found; + std::lock_guard lock(state->_mutex); + if (!state->_alive || HeldByCaller(*state)) { + return ULT_ERROR_STATE; + } + if (state->_writing) { + return ULT_ERROR_BUSY; + } + ++state->_readers; + return ULT_OK; +} + +int APS5_VABI sceUltReaderWriterLockUnlockRead(void* rwlock) { + std::shared_ptr state; + if (const int found = FindLock(rwlock, state); found != ULT_OK) return found; + std::lock_guard lock(state->_mutex); + if (!state->_alive || state->_readers == 0) { + return ULT_ERROR_STATE; + } + if (--state->_readers == 0) { + state->_released.notify_all(); + } + return ULT_OK; +} + +int APS5_VABI sceUltReaderWriterLockLockWrite(void* rwlock) { + std::shared_ptr state; + if (const int found = FindLock(rwlock, state); found != ULT_OK) return found; + std::unique_lock lock(state->_mutex); + if (!state->_alive || HeldByCaller(*state)) { + return ULT_ERROR_STATE; + } + if (const int waited = Wait(*state, lock, true); waited != ULT_OK) return waited; + state->_writing = true; + state->_writer = std::this_thread::get_id(); + return ULT_OK; +} + +int APS5_VABI sceUltReaderWriterLockTryLockWrite(void* rwlock) { + std::shared_ptr state; + if (const int found = FindLock(rwlock, state); found != ULT_OK) return found; + std::lock_guard lock(state->_mutex); + if (!state->_alive || HeldByCaller(*state)) { + return ULT_ERROR_STATE; + } + if (state->_writing || state->_readers != 0) { + return ULT_ERROR_BUSY; + } + state->_writing = true; + state->_writer = std::this_thread::get_id(); + return ULT_OK; +} + +int APS5_VABI sceUltReaderWriterLockUnlockWrite(void* rwlock) { + std::shared_ptr state; + if (const int found = FindLock(rwlock, state); found != ULT_OK) return found; + std::lock_guard lock(state->_mutex); + if (!state->_alive || !HeldByCaller(*state)) { + return ULT_ERROR_STATE; + } + state->_writing = false; + state->_writer = {}; + state->_released.notify_all(); + return ULT_OK; +} + +} diff --git a/core/libs/prx/libSceUlt/src/ReaderWriterLock.hpp b/core/libs/prx/libSceUlt/src/ReaderWriterLock.hpp new file mode 100644 index 0000000000..df98dcd077 --- /dev/null +++ b/core/libs/prx/libSceUlt/src/ReaderWriterLock.hpp @@ -0,0 +1,8 @@ +#ifndef CORE_LIBS_PRX_LIBSCEULT_SRC_READERWRITERLOCK_HPP +#define CORE_LIBS_PRX_LIBSCEULT_SRC_READERWRITERLOCK_HPP + +bool UltWaitingPoolExists(void* pool); +bool UltReaderWriterLocksWaitOn(void* pool); +void UltFinalizeReaderWriterLocks(); + +#endif diff --git a/core/libs/prx/libSceUrlConfigResolver/Export.cpp b/core/libs/prx/libSceUrlConfigResolver/Export.cpp index d81f4a5e32..9d7da335dc 100644 --- a/core/libs/prx/libSceUrlConfigResolver/Export.cpp +++ b/core/libs/prx/libSceUrlConfigResolver/Export.cpp @@ -7,4 +7,25 @@ extern "C" { APS5_DUMMY_FUN + +int APS5_VABI sceUrlConfigResolverGetReleaseVersion() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUrlConfigResolverGetSysVer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUrlConfigResolverIsUpdated() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceUrlConfigResolverUpdate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } 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/libSceUserService/Export.cpp b/core/libs/prx/libSceUserService/Export.cpp index 9f258f9aa8..7a4c11d33e 100644 --- a/core/libs/prx/libSceUserService/Export.cpp +++ b/core/libs/prx/libSceUserService/Export.cpp @@ -148,8 +148,11 @@ int APS5_VABI sceUserServiceGetUserNumber(int user_id, int32_t* number) { return USER_SERVICE_OK; } -int APS5_VABI sceUserServiceInitialize(const void* params) { - (void)params; +static bool ValidThreadPriority(int priority) { + return priority >= USER_SERVICE_THREAD_PRIORITY_MIN && priority <= USER_SERVICE_THREAD_PRIORITY_MAX; +} + +static int MarkInitialized() { if (initialized.exchange(true)) { return USER_SERVICE_ERROR_ALREADY_INITIALIZED; } @@ -157,11 +160,22 @@ int APS5_VABI sceUserServiceInitialize(const void* params) { return USER_SERVICE_OK; } -int APS5_VABI sceUserServiceInitialize2(void) { - if (!initialized.exchange(true)) { - loginReported.store(false); +int APS5_VABI sceUserServiceInitialize(const void* params) { + if (params != nullptr) { + int priority = 0; + std::memcpy(&priority, params, sizeof(priority)); + if (!ValidThreadPriority(priority)) { + return USER_SERVICE_ERROR_INVALID_ARGUMENT; + } } - return USER_SERVICE_OK; + return MarkInitialized(); +} + +int APS5_VABI sceUserServiceInitialize2(int priority) { + if (!ValidThreadPriority(priority)) { + return USER_SERVICE_ERROR_INVALID_ARGUMENT; + } + return MarkInitialized(); } int APS5_VABI sceUserServiceTerminate(void) { diff --git a/core/libs/prx/libSceUserService/UserService.hpp b/core/libs/prx/libSceUserService/UserService.hpp index ae6468367d..1e0f926aa3 100644 --- a/core/libs/prx/libSceUserService/UserService.hpp +++ b/core/libs/prx/libSceUserService/UserService.hpp @@ -12,6 +12,8 @@ constexpr int USER_SERVICE_ERROR_NO_EVENT = -2137653241; constexpr int USER_SERVICE_ERROR_NOT_LOGGED_IN = -2137653239; constexpr int USER_SERVICE_USER_ID_INVALID = -1; constexpr int USER_SERVICE_INITIAL_USER_ID = 0x10000000; +constexpr int USER_SERVICE_THREAD_PRIORITY_MIN = 0x100; +constexpr int USER_SERVICE_THREAD_PRIORITY_MAX = 0x2FF; constexpr int USER_SERVICE_ERROR_BUFFER_TOO_SHORT = -2137653238; constexpr int USER_SERVICE_ERROR_UNKNOWN_USER = static_cast(0x80960105u); constexpr std::size_t USER_SERVICE_MAX_USER_NAME_LENGTH = 16; diff --git a/core/libs/prx/libSceVideoCoreInterface/Export.cpp b/core/libs/prx/libSceVideoCoreInterface/Export.cpp index d81f4a5e32..e4fd254e95 100644 --- a/core/libs/prx/libSceVideoCoreInterface/Export.cpp +++ b/core/libs/prx/libSceVideoCoreInterface/Export.cpp @@ -7,4 +7,20 @@ extern "C" { APS5_DUMMY_FUN + +int APS5_VABI sceVideoCoreIfInitializeInterface() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoCoreIfMainLoop() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoCoreIfSetFunctionTable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceVideoOut/CMakeLists.txt b/core/libs/prx/libSceVideoOut/CMakeLists.txt index e630686244..2b3dd27800 100644 --- a/core/libs/prx/libSceVideoOut/CMakeLists.txt +++ b/core/libs/prx/libSceVideoOut/CMakeLists.txt @@ -6,6 +6,7 @@ add_library(libSceVideoOut SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/src/VideoOutDriver.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/DisplayWindow.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/PadInput.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Telemetry.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/MouseInput.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/KeyboardInput.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/ImeTextInput.cpp @@ -19,7 +20,9 @@ target_include_directories(libSceVideoOut PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} + ${CMAKE_BINARY_DIR}/core/libs/prx/libSceAgcDriver/generated ) +add_dependencies(libSceVideoOut agc_trace_revision) target_link_libraries(libSceVideoOut PRIVATE libSceAgcDriver libScePad libSceMouse libSceKeyboard libSceIme libc libkernel decoder_png SDL2-static) if(WIN32) target_link_libraries(libSceVideoOut PRIVATE comctl32 dxgi) @@ -146,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) @@ -228,6 +240,9 @@ if(BUILD_TESTING) add_test_executable(video_out_hdr_probe_tests tests/HdrProbe.cpp) target_include_directories(video_out_hdr_probe_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR}) target_link_libraries(video_out_hdr_probe_tests PRIVATE libSceVideoOut libSceAgcDriver libc SDL2-static) + if(WIN32) + target_link_libraries(video_out_hdr_probe_tests PRIVATE dxgi) + endif() set_target_properties(video_out_hdr_probe_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(video_out_hdr_probe_tests) add_test(NAME video_out_hdr_probe COMMAND video_out_hdr_probe_tests) @@ -261,3 +276,14 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +if(BUILD_TESTING AND NOT WIN32) + add_test_executable(video_out_telemetry_tests tests/Telemetry.cpp) + target_include_directories(video_out_telemetry_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR}) + target_link_libraries(video_out_telemetry_tests PRIVATE libSceVideoOut libSceAgcDriver libkernel libc SDL2-static) + set_target_properties(video_out_telemetry_tests PROPERTIES CXX_EXTENSIONS OFF) + add_test(NAME video_out_telemetry COMMAND video_out_telemetry_tests) + add_test(NAME video_out_telemetry_caption COMMAND video_out_telemetry_tests caption) + set_tests_properties(video_out_telemetry PROPERTIES TIMEOUT 30) + set_tests_properties(video_out_telemetry_caption PROPERTIES TIMEOUT 30 SKIP_RETURN_CODE 77 RESOURCE_LOCK videoout_app0 ENVIRONMENT "SDL_VIDEODRIVER=${ANYPS5_VIDEO_OUT_TEST_DRIVER}") +endif() diff --git a/core/libs/prx/libSceVideoOut/Unimplemented.cpp b/core/libs/prx/libSceVideoOut/Unimplemented.cpp index ecf6a6ce1b..dabea7e04a 100644 --- a/core/libs/prx/libSceVideoOut/Unimplemented.cpp +++ b/core/libs/prx/libSceVideoOut/Unimplemented.cpp @@ -62,4 +62,10 @@ int APS5_VABI sceVideoOutAdjustColorSpaceConversion_(int handle, const VideoOutC return sceVideoOutAdjustColorSpaceConversion(handle, settings); } + +int APS5_VABI sceDbgVideoOutAddOutputModeVr60Privilege() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceVideoOut/include/DisplayWindow.hpp b/core/libs/prx/libSceVideoOut/include/DisplayWindow.hpp index 18cfc77f12..63ba3f0281 100644 --- a/core/libs/prx/libSceVideoOut/include/DisplayWindow.hpp +++ b/core/libs/prx/libSceVideoOut/include/DisplayWindow.hpp @@ -20,6 +20,7 @@ public: SDL_Window* Handle() const; void DrawableSize(std::uint32_t& width, std::uint32_t& height) const; void UpdateTitle(); + std::uint64_t FrameNumber() const { return frameNum; } void ToggleFullscreen(); private: @@ -34,6 +35,7 @@ private: SDL_Window* window = nullptr; std::uint32_t aspectWidth = 0; std::uint32_t aspectHeight = 0; + std::uint64_t frameNum = 0; }; #endif diff --git a/core/libs/prx/libSceVideoOut/include/PadInput.hpp b/core/libs/prx/libSceVideoOut/include/PadInput.hpp index de1c799e0e..dfa068d16d 100644 --- a/core/libs/prx/libSceVideoOut/include/PadInput.hpp +++ b/core/libs/prx/libSceVideoOut/include/PadInput.hpp @@ -5,7 +5,9 @@ #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" class DisplayWindow; @@ -14,12 +16,12 @@ public: PadInput(); ~PadInput(); void HandleEvent(const SDL_Event& event, DisplayWindow& window); - void Update(); + void Update(const DisplayWindow& window); bool MouseModeEnabled() const { return keyboardMouse.MouseMode(); } private: explicit PadInput(Pad::InputConfiguration configuration); - void publish(); + void publish(std::uint64_t frame); void setMouseMode(bool enabled); void openFirstAvailableController(); void openController(int deviceIndex); @@ -34,7 +36,10 @@ private: std::uint32_t outputSequence = 0; PadOutputState outputState{}; 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/Telemetry.hpp b/core/libs/prx/libSceVideoOut/include/Telemetry.hpp new file mode 100644 index 0000000000..186cf20310 --- /dev/null +++ b/core/libs/prx/libSceVideoOut/include/Telemetry.hpp @@ -0,0 +1,74 @@ +#ifndef CORE_LIBS_PRX_LIBSCEVIDEOOUT_INCLUDE_TELEMETRY_HPP +#define CORE_LIBS_PRX_LIBSCEVIDEOOUT_INCLUDE_TELEMETRY_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "prx/libScePad/include/PadState.hpp" + +namespace VideoOutTelemetry { + +class BoundedLineQueue { +public: + explicit BoundedLineQueue(std::size_t capacity); + bool TryPush(std::string line); + bool Push(std::string line); + std::deque Drain(); + std::uint64_t Dropped() const; + +private: + bool pushLocked(std::string line); + + std::mutex mutex; + std::deque lines; + std::size_t capacity; + std::atomic dropped{0}; +}; + +class Writer { +public: + Writer(const std::string& directory, std::size_t capacity, const std::string& commit); + ~Writer(); + Writer(const Writer&) = delete; + Writer& operator=(const Writer&) = delete; + + bool EmitPad(std::uint64_t frame, const PadInputState& state); + void EmitPresent(std::uint64_t frame, double fps); + std::uint64_t Dropped() const; + +private: + bool emit(std::string line, bool mustKeep); + void run(std::stop_token token); + void writeAll(const std::string& text); + void writeDrops(bool force); + + std::ofstream output; + BoundedLineQueue queue; + std::uint64_t reportedDrops = 0; + std::jthread thread; +}; + +class PadChangeFilter { +public: + bool Changed(const PadInputState& state) const; + void Commit(const PadInputState& state); + +private: + PadInputState last{}; +}; + +bool EmitPadIfChanged(Writer& writer, PadChangeFilter& filter, std::uint64_t frame, const PadInputState& state); + +std::unique_ptr OpenFromEnvironment(const char* directory, const std::string& commit); +Writer* Instance(); + +} + +#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 5cd47dbc81..35c6c86917 100644 --- a/core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp +++ b/core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp @@ -1,10 +1,11 @@ #include "prx/libSceVideoOut/include/DisplayWindow.hpp" +#include "prx/libSceVideoOut/include/Telemetry.hpp" #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 @@ -106,22 +107,22 @@ 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 std::uint64_t frameNum = 0; + 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; } char text[160]; std::snprintf(text, sizeof(text), "%s | FPS: %.2f (%llu)", title.value, currentFps, static_cast(frameNum)); SDL_SetWindowTitle(window, text); + if (auto* telemetry = VideoOutTelemetry::Instance(); telemetry != nullptr && (frameNum == 1 || frameNum % 30 == 0)) telemetry->EmitPresent(frameNum, currentFps); } void DisplayWindow::installSubclass() { 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 5478ece435..d9abeeb048 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); } @@ -185,12 +192,12 @@ PadInputState PadInput::sampleController() const { float pressure = 0.0f; if (SDL_GameControllerGetTouchpadFinger(controller, 0, finger, &down, &x, &y, &pressure) != 0 || down == 0) continue; result.touch[finger].active = true; - result.touch[finger].x = static_cast(std::clamp(x, 0.0f, 1.0f) * 1919.0f); - result.touch[finger].y = static_cast(std::clamp(y, 0.0f, 1.0f) * 942.0f); + result.touch[finger].x = static_cast(std::clamp(x, 0.0f, 1.0f) * (kTouchPadWidth - 1)); + result.touch[finger].y = static_cast(std::clamp(y, 0.0f, 1.0f) * (kTouchPadHeight - 1)); } } if (viewPressed && !result.touch[0].active && !result.touch[1].active) { - result.touch[0] = {true, 960, 471, 0}; + result.touch[0] = {true, kTouchPadWidth / 2, kTouchPadHeight / 2, 0}; } return result; } @@ -205,18 +212,18 @@ void PadInput::HandleEvent(const SDL_Event& event, DisplayWindow& window) { if (instanceId == event.cdevice.which) { closeController(); openFirstAvailableController(); - publish(); + publish(window.FrameNumber()); } return; } if (event.type == SDL_CONTROLLERBUTTONDOWN || event.type == SDL_CONTROLLERBUTTONUP || event.type == SDL_CONTROLLERAXISMOTION || event.type == SDL_CONTROLLERDEVICEREMAPPED) { - publish(); + publish(window.FrameNumber()); return; } if (event.type == SDL_WINDOWEVENT) { if (keyboardMouse.HandleEvent(event, std::chrono::steady_clock::now()).releaseMouse) setMouseMode(false); - publish(); + publish(window.FrameNumber()); return; } // APS5_NO_PAD_INPUT=1 keeps a measurement run from reacting to keys or mouse buttons that reach @@ -230,24 +237,27 @@ 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(); + if (event.type != SDL_MOUSEMOTION) publish(window.FrameNumber()); } -void PadInput::Update() { +void PadInput::Update(const DisplayWindow& window) { if (controller != nullptr) SDL_GameControllerUpdate(); applyOutput(); if (controller != nullptr) controllerState = sampleController(); if (keyboardMouse.MouseMode() && SDL_GetKeyboardFocus() == nullptr) { keyboardMouse.Release(); setMouseMode(false); - publish(); + publish(window.FrameNumber()); return; } const bool changed = keyboardMouse.Advance(std::chrono::steady_clock::now()); - if (controller != nullptr || keyboardMouse.MouseMode() || changed) publish(); + if (controller != nullptr || keyboardMouse.MouseMode() || changed) publish(window.FrameNumber()); } -void PadInput::publish() { - PadPublishInput_nid_postfix(keyboardMouse.Compose(controllerState)); +void PadInput::publish(std::uint64_t frame) { + const auto state = keyboardMouse.Compose(controllerState); + PadPublishInput_nid_postfix(state); + if (auto* telemetry = VideoOutTelemetry::Instance(); telemetry != nullptr) VideoOutTelemetry::EmitPadIfChanged(*telemetry, telemetryFilter, frame, state); } diff --git a/core/libs/prx/libSceVideoOut/src/Telemetry.cpp b/core/libs/prx/libSceVideoOut/src/Telemetry.cpp new file mode 100644 index 0000000000..5a1130a7bc --- /dev/null +++ b/core/libs/prx/libSceVideoOut/src/Telemetry.cpp @@ -0,0 +1,195 @@ +#include "prx/libSceVideoOut/include/Telemetry.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#endif + +#include "prx/libSceAgcDriver/TraceRevision.hpp" + +namespace VideoOutTelemetry { + +namespace { + +constexpr char ContractLevel[] = "v1-pad"; + +constexpr auto DropReportInterval = std::chrono::seconds(10); +constexpr std::size_t DefaultQueueCapacity = 4096; + +std::int64_t steadyNanos() { + return std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count(); +} + +std::int64_t processId() { +#ifdef _WIN32 + return static_cast(::GetCurrentProcessId()); +#else + return static_cast(::getpid()); +#endif +} + +std::string head(std::int64_t timeNanos, const char* event) { + char text[96]; + std::snprintf(text, sizeof(text), "{\"v\":1,\"t_ns\":%lld,\"ev\":\"%s\"", static_cast(timeNanos), event); + return text; +} + +} + +BoundedLineQueue::BoundedLineQueue(std::size_t capacity) : capacity(capacity) { +} + +bool BoundedLineQueue::TryPush(std::string line) { + std::unique_lock lock(mutex, std::try_to_lock); + if (!lock.owns_lock()) { + dropped.fetch_add(1, std::memory_order_relaxed); + return false; + } + return pushLocked(std::move(line)); +} + +bool BoundedLineQueue::Push(std::string line) { + std::lock_guard lock(mutex); + return pushLocked(std::move(line)); +} + +bool BoundedLineQueue::pushLocked(std::string line) { + if (lines.size() >= capacity) { + dropped.fetch_add(1, std::memory_order_relaxed); + return false; + } + lines.push_back(std::move(line)); + return true; +} + +std::deque BoundedLineQueue::Drain() { + std::lock_guard lock(mutex); + return std::exchange(lines, {}); +} + +std::uint64_t BoundedLineQueue::Dropped() const { + return dropped.load(std::memory_order_relaxed); +} + +Writer::Writer(const std::string& directory, std::size_t capacity, const std::string& commit) : queue(capacity) { + const std::string path = directory + "/videoout.jsonl"; + output.open(path, std::ios::binary | std::ios::app); + if (!output.is_open()) throw std::runtime_error("VideoOut telemetry: cannot open " + path); + output.exceptions(std::ios::badbit | std::ios::failbit); + const auto steady = steadyNanos(); + char text[256]; + std::snprintf(text, sizeof(text), ",\"contract\":\"%s\",\"commit\":\"%s\",\"pid\":%lld,\"prx\":\"libSceVideoOut\"}\n", ContractLevel, commit.c_str(), static_cast(processId())); + queue.Push(head(steady, "contract") + text); + thread = std::jthread([this](std::stop_token token) { run(token); }); +} + +Writer::~Writer() { + thread.request_stop(); + if (thread.joinable()) thread.join(); +} + +bool Writer::emit(std::string line, bool mustKeep) { + return mustKeep ? queue.Push(std::move(line)) : queue.TryPush(std::move(line)); +} + +void Writer::writeAll(const std::string& text) { + output.write(text.data(), static_cast(text.size())); + output.flush(); +} + +void Writer::writeDrops(bool force) { + const auto total = queue.Dropped(); + if (total == 0 || (total == reportedDrops && !force)) return; + reportedDrops = total; + char text[64]; + std::snprintf(text, sizeof(text), ",\"dropped\":%llu}\n", static_cast(total)); + writeAll(head(steadyNanos(), "drops") + text); +} + +void Writer::run(std::stop_token token) { + try { + auto nextDropReport = std::chrono::steady_clock::now() + DropReportInterval; + std::mutex sleepMutex; + std::condition_variable_any sleepWake; + while (true) { + { + std::unique_lock lock(sleepMutex); + sleepWake.wait_for(lock, token, std::chrono::milliseconds(200), [] { return false; }); + } + std::string batch; + for (auto& line : queue.Drain()) batch += line; + if (!batch.empty()) writeAll(batch); + if (std::chrono::steady_clock::now() >= nextDropReport) { + nextDropReport += DropReportInterval; + writeDrops(false); + } + if (token.stop_requested()) break; + } + std::string rest; + for (auto& line : queue.Drain()) rest += line; + if (!rest.empty()) writeAll(rest); + writeDrops(true); + } catch (const std::exception& error) { + std::fprintf(stderr, "[videoout] telemetry writer stopped: %s\n", error.what()); + } catch (...) { + std::fprintf(stderr, "[videoout] telemetry writer stopped\n"); + } +} + +bool Writer::EmitPad(std::uint64_t frame, const PadInputState& state) { + char text[192]; + std::snprintf(text, sizeof(text), ",\"n\":%llu,\"buttons\":%u,\"sticks\":[%u,%u,%u,%u],\"triggers\":[%u,%u]}\n", + static_cast(frame), state.buttons, state.sticks[0], state.sticks[1], state.sticks[2], state.sticks[3], + state.analogButtonsL2, state.analogButtonsR2); + return emit(head(steadyNanos(), "pad") + text, true); +} + +void Writer::EmitPresent(std::uint64_t frame, double fps) { + char text[96]; + std::snprintf(text, sizeof(text), ",\"n\":%llu,\"fps\":%.2f}\n", static_cast(frame), fps); + emit(head(steadyNanos(), "present") + text, false); +} + +std::uint64_t Writer::Dropped() const { + return queue.Dropped(); +} + +bool PadChangeFilter::Changed(const PadInputState& state) const { + return state.buttons != last.buttons || state.sticks != last.sticks || + state.analogButtonsL2 != last.analogButtonsL2 || state.analogButtonsR2 != last.analogButtonsR2; +} + +void PadChangeFilter::Commit(const PadInputState& state) { + last.buttons = state.buttons; + last.sticks = state.sticks; + last.analogButtonsL2 = state.analogButtonsL2; + last.analogButtonsR2 = state.analogButtonsR2; +} + +bool EmitPadIfChanged(Writer& writer, PadChangeFilter& filter, std::uint64_t frame, const PadInputState& state) { + if (!filter.Changed(state)) return false; + if (!writer.EmitPad(frame, state)) return false; + filter.Commit(state); + return true; +} + +std::unique_ptr OpenFromEnvironment(const char* directory, const std::string& commit) { + if (directory == nullptr || directory[0] == '\0') return nullptr; + return std::make_unique(directory, DefaultQueueCapacity, commit); +} + +Writer* Instance() { + static const std::unique_ptr writer = OpenFromEnvironment(std::getenv("APS5_VIDEOOUT_TELEMETRY_DIR"), APS5_TRACE_COMMIT); + return writer.get(); +} + +} diff --git a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp index 88c960113f..f308f42739 100644 --- a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp +++ b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp @@ -19,6 +19,7 @@ #include "prx/libScePad/include/PadState.hpp" #include "prx/libkernel/Equeue/Equeue.hpp" #include "prx/libkernel/Time/include/Time.hpp" +#include "prx/libkernel/Time/include/SleepWake.hpp" #include "prx/libSceVideoOut/include/VideoOutDriver.hpp" #if APS5_ENABLE_TIMING_LOG #include "prx/libSceVideoOut/include/FrameTimingLog.hpp" @@ -271,6 +272,7 @@ VideoOutDriver::VideoOutDriver() { SDL_SetHint(SDL_HINT_WINDOWS_DPI_AWARENESS, "permonitorv2"); #ifdef _WIN32 SDL_SetHintWithPriority(SDL_HINT_DIRECTINPUT_ENABLED, "0", SDL_HINT_DEFAULT); + if (std::getenv("APS5_NO_WGI") != nullptr) SDL_SetHintWithPriority(SDL_HINT_JOYSTICK_WGI, "0", SDL_HINT_DEFAULT); #endif #ifdef __linux__ SDL_SetHint(SDL_HINT_VIDEODRIVER, "wayland,x11"); @@ -540,7 +542,11 @@ void VideoOutDriver::processFlip(FlipRequest& req) { req.ReleaseLocked(); return; } - require(req.cfg->outputMode != VIDEO_OUT_OUTPUT_MODE_119_88HZ || SupportsHighFrameRate(), "display no longer supports 119.88 Hz output"); + if (req.cfg->outputMode == VIDEO_OUT_OUTPUT_MODE_119_88HZ && !SupportsHighFrameRate()) { + req.cfg->outputMode = VIDEO_OUT_OUTPUT_MODE_DEFAULT; + triggerEvents(*req.cfg, VIDEO_OUT_EVENT_SET_MODE, reinterpret_cast(req.cfg->outputMode)); + std::fprintf(stderr, "[videoout] display no longer supports 119.88 Hz; fell back to 59.94 Hz\n"); + } require(req.ready && !req.terminal && req.generation == req.cfg->generation, "stale or incomplete flip request"); const auto interval = static_cast(req.flipRate + 1); require(req.cfg->lastFlipVblank <= std::numeric_limits::max() - interval, "flip interval overflow"); @@ -618,6 +624,7 @@ void VideoOutDriver::processFlip(FlipRequest& req) { } req.terminal = true; req.cfg->vblankCond.notify_all(); + KernelWakeSleepers_nid_no_patch(); timing.Mark("notify_game"); } @@ -673,7 +680,7 @@ void VideoOutDriver::presentLoop(std::stop_token token, std::promise& star imeTextInput.HandleEvent(event, SDL_GetWindowID(window.Handle())); } } - padInput.Update(); + padInput.Update(window); updateDisplayRefreshRate(); pollHostHdr(); mouseInput.UpdateCapture(!padInput.MouseModeEnabled()); diff --git a/core/libs/prx/libSceVideoOut/tests/Flip.cpp b/core/libs/prx/libSceVideoOut/tests/Flip.cpp index b86554b599..d23a315114 100644 --- a/core/libs/prx/libSceVideoOut/tests/Flip.cpp +++ b/core/libs/prx/libSceVideoOut/tests/Flip.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #undef main @@ -681,7 +682,12 @@ struct BusyLoop { std::vector commands; }; -BusyLoop prepareBusyLoop(std::uint32_t iterations) { +constexpr std::uint32_t busyLoopTotalIterations = 100000000u; +constexpr std::uint32_t busyLoopDispatchCount = 100u; +constexpr std::uint32_t busyLoopIterationsPerDispatch = busyLoopTotalIterations / busyLoopDispatchCount; +static_assert(busyLoopTotalIterations % busyLoopDispatchCount == 0); + +BusyLoop prepareBusyLoop(std::uint32_t iterations, std::uint32_t dispatchCount = 1u) { alignas(256) static const std::array code{ 0xbe850380, 0x7e040281, 0xd5690002, 0x00020502, 0x4a040481, 0x80058105, 0xbf070405, 0xbf85fffa, 0x7e020280, 0xe0701000, 0x80000201, 0xbf810000}; @@ -703,13 +709,17 @@ BusyLoop prepareBusyLoop(std::uint32_t iterations) { const auto program = reinterpret_cast(code.data()); const auto output = reinterpret_cast(result); const auto target = reinterpret_cast(label); - return {result, label, { + std::vector commands{ 0xc0027600, 0x20c, static_cast(program >> 8u), static_cast(program >> 40u), 0xc0017600, 0x213, 5u << 1u, 0xc0037600, 0x207, 32, 1, 1, 0xc0057600, 0x240, static_cast(output), static_cast(output >> 32u) & 0xffffu, 4, 0x31016fac, iterations, - 0xc0031500, 1, 1, 1, 0x8041, - 0xc0064900, 0x514, 1u << 29u, static_cast(target), static_cast(target >> 32u), 1, 0, 0}}; + }; + for (std::uint32_t dispatch = 0; dispatch < dispatchCount; ++dispatch) { + commands.insert(commands.end(), {0xc0031500, 1, 1, 1, 0x8041}); + } + commands.insert(commands.end(), {0xc0064900, 0x514, 1u << 29u, static_cast(target), static_cast(target >> 32u), 1, 0, 0}); + return {result, label, std::move(commands)}; } void testReleaseVblank() { @@ -723,7 +733,7 @@ void testReleaseVblank() { sceVideoOutSetBufferAttribute2(&attribute, 0x8000000000000000ull, 0, 259, 137, 0, 0, 0); sceVideoOutRegisterBuffers2(handle, 0, 0, &buffer, 1, &attribute, 0, nullptr); check(sceVideoOutSetFlipRate(handle, 2) == 0, "flip rate was not applied"); - auto busy = prepareBusyLoop(100000000u); + auto busy = prepareBusyLoop(busyLoopIterationsPerDispatch, busyLoopDispatchCount); Packet packet{busy.commands.data(), static_cast(busy.commands.size()), 0, {}}; check(sceAgcDriverSubmitDcb(&packet) == 0, "the GPU work was not accepted"); sceVideoOutSubmitFlip(handle, 0, 1, 1); @@ -749,8 +759,8 @@ void testFlipAfterGpuWork(bool eop = false) { VideoOutBufferAttribute2 attribute{}; sceVideoOutSetBufferAttribute2(&attribute, 0x8000000000000000ull, 0, 259, 137, 0, 0, 0); sceVideoOutRegisterBuffers2(handle, 0, 0, &buffer, 1, &attribute, 0, nullptr); - constexpr std::uint32_t iterations = 100000000u; - auto busy = prepareBusyLoop(iterations); + constexpr std::uint32_t iterations = busyLoopIterationsPerDispatch; + auto busy = prepareBusyLoop(iterations, busyLoopDispatchCount); auto& result = *busy.result; auto& label = *busy.label; Packet packet{busy.commands.data(), static_cast(busy.commands.size()), 0, {}}; diff --git a/core/libs/prx/libSceVideoOut/tests/HdrProbe.cpp b/core/libs/prx/libSceVideoOut/tests/HdrProbe.cpp index 20751608dc..68d5afc3ce 100644 --- a/core/libs/prx/libSceVideoOut/tests/HdrProbe.cpp +++ b/core/libs/prx/libSceVideoOut/tests/HdrProbe.cpp @@ -4,8 +4,47 @@ #include #include +#ifdef _WIN32 +#include "SDL_syswm.h" +#include +#include +#endif + #undef main +namespace { + +#ifdef _WIN32 +bool monitorReportsHdr(SDL_Window* window) { + SDL_SysWMinfo info; + SDL_VERSION(&info.version); + if (SDL_GetWindowWMInfo(window, &info) != SDL_TRUE || info.subsystem != SDL_SYSWM_WINDOWS) return false; + const HMONITOR monitor = MonitorFromWindow(info.info.win.window, MONITOR_DEFAULTTONEAREST); + IDXGIFactory1* factory = nullptr; + if (FAILED(CreateDXGIFactory1(__uuidof(IDXGIFactory1), reinterpret_cast(&factory)))) return false; + bool hdr = false; + IDXGIAdapter1* adapter = nullptr; + for (UINT adapterIndex = 0; !hdr && SUCCEEDED(factory->EnumAdapters1(adapterIndex, &adapter)); ++adapterIndex) { + IDXGIOutput* output = nullptr; + for (UINT outputIndex = 0; !hdr && SUCCEEDED(adapter->EnumOutputs(outputIndex, &output)); ++outputIndex) { + DXGI_OUTPUT_DESC description{}; + IDXGIOutput6* output6 = nullptr; + DXGI_OUTPUT_DESC1 description1{}; + if (SUCCEEDED(output->GetDesc(&description)) && description.Monitor == monitor && + SUCCEEDED(output->QueryInterface(__uuidof(IDXGIOutput6), reinterpret_cast(&output6))) && SUCCEEDED(output6->GetDesc1(&description1))) + hdr = description1.ColorSpace == DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020; + if (output6 != nullptr) output6->Release(); + output->Release(); + } + adapter->Release(); + } + factory->Release(); + return hdr; +} +#endif + +} + int main() { if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) { std::printf("skipped, no SDL video driver: %s\n", SDL_GetError()); @@ -18,6 +57,14 @@ int main() { return 77; } const std::string driver = SDL_GetCurrentVideoDriver() != nullptr ? SDL_GetCurrentVideoDriver() : "unknown"; +#ifdef _WIN32 + if (monitorReportsHdr(window)) { + std::printf("skipped, the window's monitor reports HDR to Windows (driver %s)\n", driver.c_str()); + SDL_DestroyWindow(window); + SDL_QuitSubSystem(SDL_INIT_VIDEO); + return 77; + } +#endif AgcDriverSetSurfaceHdr10_nid_postfix(true); int result = 0; { 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/libSceVideoOut/tests/Telemetry.cpp b/core/libs/prx/libSceVideoOut/tests/Telemetry.cpp new file mode 100644 index 0000000000..d3fc3ce183 --- /dev/null +++ b/core/libs/prx/libSceVideoOut/tests/Telemetry.cpp @@ -0,0 +1,184 @@ +#include "prx/libSceVideoOut/include/DisplayWindow.hpp" +#include "prx/libSceVideoOut/include/Telemetry.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#undef main + +namespace { + +void check(bool condition, const std::string& reason) { + if (!condition) throw std::runtime_error(reason); +} + +std::vector readLines(const std::filesystem::path& path) { + std::ifstream input(path); + std::vector lines; + for (std::string line; std::getline(input, line);) lines.push_back(line); + return lines; +} + +long long field(const std::string& line, const std::string& name) { + const auto position = line.find("\"" + name + "\":"); + check(position != std::string::npos, "missing field " + name + " in " + line); + return std::stoll(line.substr(position + name.size() + 3)); +} + +std::filesystem::path freshDirectory(const char* name) { + auto directory = std::filesystem::temp_directory_path() / (std::string("aps5-videoout-telemetry-") + name + "-" + std::to_string(::getpid())); + std::filesystem::remove_all(directory); + std::filesystem::create_directories(directory); + return directory; +} + +void padChangeIgnoresSensors() { + VideoOutTelemetry::PadChangeFilter filter; + PadInputState state; + check(!filter.Changed(state), "idle state must not be a change"); + state.hasMotion = true; + state.accel = {1.0f, 2.0f, 3.0f}; + state.gyro = {0.5f, 0.5f, 0.5f}; + state.touch[0] = {true, 100, 200, 1}; + state.touchLeft = true; + check(!filter.Changed(state), "motion and touch must not be a change"); + state.buttons = 0x4000; + check(filter.Changed(state), "a button must be a change"); + check(filter.Changed(state), "an uncommitted change must stay a change"); + filter.Commit(state); + check(!filter.Changed(state), "a repeated state must not be a change"); + state.sticks[2] = 200; + check(filter.Changed(state), "a stick must be a change"); + filter.Commit(state); + state.analogButtonsR2 = 10; + check(filter.Changed(state), "a trigger must be a change"); +} + +void padChangeSurvivesFullQueue() { + const auto directory = freshDirectory("fullqueue"); + { + VideoOutTelemetry::Writer writer(directory.string(), 1, "test"); + VideoOutTelemetry::PadChangeFilter filter; + PadInputState state; + state.buttons = 0x4000; + check(!VideoOutTelemetry::EmitPadIfChanged(writer, filter, 5, state), "a full queue must refuse the pad line"); + check(filter.Changed(state), "a refused pad change must stay pending"); + check(writer.Dropped() == 1, "the refused pad line must be counted"); + std::this_thread::sleep_for(std::chrono::milliseconds(600)); + check(VideoOutTelemetry::EmitPadIfChanged(writer, filter, 6, state), "the retry must succeed once the queue drains"); + check(!filter.Changed(state), "a written pad change must be committed"); + } + const auto lines = readLines(directory / "videoout.jsonl"); + bool found = false; + for (const auto& line : lines) found = found || (line.find("\"ev\":\"pad\"") != std::string::npos && field(line, "n") == 6 && field(line, "buttons") == 0x4000); + check(found, "the retried pad line must be in the file"); + bool dropsLine = false; + for (const auto& line : lines) dropsLine = dropsLine || (line.find("\"ev\":\"drops\"") != std::string::npos && field(line, "dropped") == 1); + check(dropsLine, "the writer must report the refused line in a drops line"); + std::filesystem::remove_all(directory); +} + +void noEnvironmentOpensNothing() { + const auto directory = freshDirectory("noenv"); + check(VideoOutTelemetry::OpenFromEnvironment(nullptr, "test") == nullptr, "unset variable must not open a writer"); + check(VideoOutTelemetry::OpenFromEnvironment("", "test") == nullptr, "empty variable must not open a writer"); + check(std::filesystem::is_empty(directory), "no file may exist without the variable"); + std::filesystem::remove_all(directory); +} + +void queueOverflowIsCounted() { + VideoOutTelemetry::BoundedLineQueue queue(2); + check(queue.TryPush("a"), "first line must fit"); + check(queue.TryPush("b"), "second line must fit"); + check(!queue.TryPush("c"), "third line must overflow"); + check(!queue.TryPush("d"), "fourth line must overflow"); + check(queue.Dropped() == 2, "two overflows must be counted"); + check(queue.Drain().size() == 2, "the stored lines must survive"); + check(queue.TryPush("e"), "a drained queue must accept lines"); +} + +void contractPadAndPresentLines() { + const auto directory = freshDirectory("contract"); + { + auto writer = VideoOutTelemetry::OpenFromEnvironment(directory.c_str(), "abc123"); + check(writer != nullptr, "variable must open a writer"); + PadInputState state; + state.buttons = 0x4000; + writer->EmitPad(7, state); + writer->EmitPresent(30, 59.94); + } + const auto lines = readLines(directory / "videoout.jsonl"); + check(lines.size() == 3, "expected contract, pad and present, got " + std::to_string(lines.size())); + check(lines[0].find("\"ev\":\"contract\"") != std::string::npos, "contract must be first"); + check(lines[0].find("\"contract\":\"v1-pad\"") != std::string::npos, "contract must name the level"); + check(lines[0].find("\"commit\":\"abc123\"") != std::string::npos, "contract must carry the commit"); + check(lines[0].find("\"prx\":\"libSceVideoOut\"") != std::string::npos, "contract must name the prx"); + check(field(lines[0], "pid") > 0, "contract must carry the pid"); + check(lines[1].find("\"ev\":\"pad\"") != std::string::npos && field(lines[1], "n") == 7 && field(lines[1], "buttons") == 0x4000, "pad line"); + check(lines[1].find("\"sticks\":[128,128,128,128]") != std::string::npos, "pad sticks"); + check(lines[2].find("\"ev\":\"present\"") != std::string::npos && field(lines[2], "n") == 30, "present line"); + std::filesystem::remove_all(directory); +} + +int frameNumberMatchesCaption() { + const auto directory = freshDirectory("caption"); + std::filesystem::create_directories(directory / "app0" / "sce_sys"); + std::ofstream(directory / "app0" / "sce_sys" / "param.json") << R"({"titleId":"PPSA00000","localizedParameters":{"defaultLanguage":"en-US","en-US":{"titleName":"Telemetry Test"}}})"; + std::filesystem::current_path(directory); + if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) { + std::printf("skipped, no display or Vulkan device: SDL video driver: %s\n", SDL_GetError()); + return 77; + } + DisplayWindow window; + try { + window.Ensure(640, 360); + } catch (const std::exception& error) { + std::printf("skipped, no display or Vulkan device: %s\n", error.what()); + SDL_QuitSubSystem(SDL_INIT_VIDEO); + return 77; + } + check(window.FrameNumber() == 0, "no frame before the first title update"); + for (std::uint64_t expected = 1; expected <= 65; ++expected) { + window.UpdateTitle(); + check(window.FrameNumber() == expected, "FrameNumber must count title updates"); + const std::string title = SDL_GetWindowTitle(window.Handle()); + const auto open = title.rfind('('); + check(open != std::string::npos && std::stoull(title.substr(open + 1)) == expected, "caption counter differs from FrameNumber: " + title); + } + window.Destroy(); + SDL_QuitSubSystem(SDL_INIT_VIDEO); + std::filesystem::current_path(directory.parent_path()); + std::filesystem::remove_all(directory); + return 0; +} + +} + +int main(int argc, char** argv) { + try { + if (argc > 1 && std::string(argv[1]) == "caption") { + const int result = frameNumberMatchesCaption(); + if (result == 0) std::printf("VideoOut caption counter test passed\n"); + return result; + } + padChangeIgnoresSensors(); + padChangeSurvivesFullQueue(); + noEnvironmentOpensNothing(); + queueOverflowIsCounted(); + contractPadAndPresentLines(); + } catch (const std::exception& error) { + std::fprintf(stderr, "FAIL: %s\n", error.what()); + return 1; + } + std::printf("VideoOut telemetry tests passed\n"); + return 0; +} diff --git a/core/libs/prx/libSceVideoRecordingP/Export.cpp b/core/libs/prx/libSceVideoRecordingP/Export.cpp index 915a17728e..6d2fe29052 100644 --- a/core/libs/prx/libSceVideoRecordingP/Export.cpp +++ b/core/libs/prx/libSceVideoRecordingP/Export.cpp @@ -80,7 +80,6 @@ int APS5_VABI sceVideoRecordingGetVideoOutMode(void* mode) { } int APS5_VABI sceVideoRecordingInit(void) { - NotImplemented_nid_no_patch(__func__); return 0; } @@ -141,7 +140,6 @@ int APS5_VABI sceVideoRecordingRestModeProcessStart(void* param) { int APS5_VABI sceVideoRecordingSendCommand(int cmd, const void* param) { (void)cmd; (void)param; - NotImplemented_nid_no_patch(__func__); return 0; } @@ -164,7 +162,6 @@ int APS5_VABI sceVideoRecordingStop(void) { } int APS5_VABI sceVideoRecordingTerm(void) { - NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceVideodec/CMakeLists.txt b/core/libs/prx/libSceVideodec/CMakeLists.txt new file mode 100644 index 0000000000..9289cda4e6 --- /dev/null +++ b/core/libs/prx/libSceVideodec/CMakeLists.txt @@ -0,0 +1,20 @@ +add_library(libSceVideodec SHARED EXCLUDE_FROM_ALL + ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp +) + +target_include_directories(libSceVideodec PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceVideodec) + +target_link_libraries(libSceVideodec PRIVATE libSceVideodec2) + +if(BUILD_TESTING) + add_test_executable(guest_videodec_tests ${LIBS_INCLUDE_DIR}/tests/GuestVideodec.cpp) + target_include_directories(guest_videodec_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_videodec_tests PRIVATE libSceVideodec libSceVideodec2 libc) + configure_windows_unwind(guest_videodec_tests) + add_test(NAME guest_videodec COMMAND guest_videodec_tests) + if(WIN32) + set_tests_properties(guest_videodec PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceVideodec/Export.cpp b/core/libs/prx/libSceVideodec/Export.cpp index 8f20f3fc69..56919ce362 100644 --- a/core/libs/prx/libSceVideodec/Export.cpp +++ b/core/libs/prx/libSceVideodec/Export.cpp @@ -1,27 +1,413 @@ #include #include +#include +#include +#include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +constexpr int VideodecErrorApiFail = static_cast(0x80C10000); +constexpr int VideodecErrorStructSize = static_cast(0x80C10002); +constexpr int VideodecErrorHandle = static_cast(0x80C10003); +constexpr int VideodecErrorAuSize = static_cast(0x80C10009); +constexpr int VideodecErrorAuPointer = static_cast(0x80C1000A); +constexpr int VideodecErrorFrameBufferSize = static_cast(0x80C1000B); +constexpr int VideodecErrorFrameBufferPointer = static_cast(0x80C1000C); +constexpr int VideodecErrorConfigInfo = static_cast(0x80C1000E); +constexpr int VideodecErrorArgumentPointer = static_cast(0x80C1000F); +constexpr int VideodecErrorDecodeAu = static_cast(0x80C10011); + +constexpr std::uint32_t CodecAvc = 1; +constexpr std::uint32_t PitchAlignment = 64; + +struct ConfigInfo { + std::uint64_t thisSize; + std::uint32_t codecType; + std::uint32_t profile; + std::uint32_t maxLevel; + std::int32_t maxFrameWidth; + std::int32_t maxFrameHeight; + std::int32_t maxDpbFrameCount; + std::uint64_t videodecFlags; +}; + +struct ResourceInfo { + std::uint64_t thisSize; + std::uint64_t cpuMemorySize; + void* cpuMemory; + std::uint64_t cpuGpuMemorySize; + void* cpuGpuMemory; + std::uint64_t maxFrameBufferSize; + std::uint32_t frameBufferAlignment; +}; + +struct Ctrl { + std::uint64_t thisSize; + void* handle; + std::uint64_t version; +}; + +struct FrameBuffer { + std::uint64_t thisSize; + void* frameBuffer; + std::uint64_t frameBufferSize; +}; + +struct AvcInfo { + std::uint32_t numUnitsInTick; + std::uint32_t timeScale; + std::uint8_t fixedFrameRateFlag; + std::uint8_t aspectRatioIdc; + std::uint16_t sarWidth; + std::uint16_t sarHeight; + std::uint8_t colourPrimaries; + std::uint8_t transferCharacteristics; + std::uint8_t matrixCoefficients; + std::uint8_t videoFullRangeFlag; + std::uint32_t frameCropLeftOffset; + std::uint32_t frameCropRightOffset; + std::uint32_t frameCropTopOffset; + std::uint32_t frameCropBottomOffset; +}; + +union CodecInfo { + std::uint8_t reserved[64]; + AvcInfo avc; +}; + +struct PictureInfo { + std::uint64_t thisSize; + std::uint32_t isValid; + std::uint32_t codecType; + std::uint32_t frameWidth; + std::uint32_t framePitch; + std::uint32_t frameHeight; + std::uint32_t isErrorPic; + std::uint64_t ptsData; + std::uint64_t attachedData; + CodecInfo codec; +}; + +struct InputData { + std::uint64_t thisSize; + const std::uint8_t* auData; + std::uint64_t auSize; + std::uint64_t ptsData; + std::uint64_t dtsData; + std::uint64_t attachedData; +}; + +struct Decoder2ConfigInfo { + std::uint64_t thisSize; + std::uint32_t resourceType; + std::uint32_t codecType; + std::uint32_t profile; + std::uint32_t maxLevel; + std::int32_t maxFrameWidth; + std::int32_t maxFrameHeight; + std::int32_t maxDpbFrameCount; + std::uint32_t decodePipelineDepth; +}; + +struct Decoder2MemoryInfo { + std::uint64_t thisSize; + std::uint64_t cpuMemorySize; + void* cpuMemory; + std::uint64_t gpuMemorySize; + void* gpuMemory; + std::uint64_t cpuGpuMemorySize; + void* cpuGpuMemory; + std::uint64_t maxFrameBufferSize; + std::uint32_t frameBufferAlignment; +}; + +struct Decoder2FrameBuffer { + std::uint64_t thisSize; + void* frameBuffer; + std::uint64_t frameBufferSize; + bool isAccepted; +}; + +struct Decoder2OutputInfo { + std::uint64_t thisSize; + bool isValid; + bool isErrorFrame; + std::uint8_t pictureCount; + std::uint32_t codecType; + std::uint32_t frameWidth; + std::uint32_t framePitch; + std::uint32_t frameHeight; + void* frameBuffer; + std::uint64_t frameBufferSize; + std::uint32_t frameFormat; + std::uint32_t framePitchInBytes; +}; + +struct Decoder2AvcPictureInfo { + std::uint64_t thisSize; + bool isValid; + std::uint64_t ptsData; + std::uint64_t dtsData; + std::uint64_t attachedData; + std::uint8_t idrPictureFlag; + std::uint8_t profileIdc; + std::uint8_t levelIdc; + std::uint32_t picWidthInLumaSamples; + std::uint32_t picHeightInLumaSamples; +}; + +struct Decoder2Crop { + std::uint32_t left; + std::uint32_t right; + std::uint32_t top; + std::uint32_t bottom; +}; + +static_assert(sizeof(ConfigInfo) == 40 && sizeof(ResourceInfo) == 56 && sizeof(Ctrl) == 24); +static_assert(sizeof(FrameBuffer) == 24 && sizeof(InputData) == 48 && sizeof(PictureInfo) == 112); +static_assert(sizeof(CodecInfo) == 64 && offsetof(PictureInfo, codec) == 48); + +struct Decoder { + std::uint64_t handle = 0; + std::vector picture; +}; + +std::mutex decodersMutex; +std::map decoders; + +Decoder* FindDecoder(const void* handle) { + if (handle == nullptr) return nullptr; + std::lock_guard lock(decodersMutex); + const auto found = decoders.find(handle); + return found == decoders.end() ? nullptr : found->second; +} + +std::uint32_t AlignUp(std::uint32_t value, std::uint32_t alignment) { + return (value + alignment - 1) / alignment * alignment; +} + +} + +extern "C" { +int APS5_VABI sceVideodec2QueryDecoderMemoryInfo_nid_postfix(const Decoder2ConfigInfo*, Decoder2MemoryInfo*); +int APS5_VABI sceVideodec2CreateDecoder_nid_postfix(const Decoder2ConfigInfo*, const Decoder2MemoryInfo*, std::uint64_t*); +int APS5_VABI sceVideodec2DeleteDecoder_nid_postfix(std::uint64_t); +int APS5_VABI sceVideodec2Decode_nid_postfix(std::uint64_t, const InputData*, Decoder2FrameBuffer*, Decoder2OutputInfo*); +int APS5_VABI sceVideodec2Flush_nid_postfix(std::uint64_t, Decoder2FrameBuffer*, Decoder2OutputInfo*); +int APS5_VABI sceVideodec2Reset_nid_postfix(std::uint64_t); +int APS5_VABI sceVideodec2GetAvcPictureInfo_nid_postfix(const Decoder2OutputInfo*, Decoder2AvcPictureInfo*, Decoder2AvcPictureInfo*); +void Videodec2PictureCrop_nid_no_patch(std::uint64_t, const void*, Decoder2Crop*); +} + +namespace { + +struct Geometry { + std::uint32_t width = 0; + std::uint32_t height = 0; + std::uint32_t pitch = 0; + Decoder2Crop crop{}; +}; + +Geometry Measure(const Decoder2OutputInfo& output, const Decoder2Crop& crop) { + Geometry geometry; + geometry.width = output.frameWidth; + geometry.height = output.frameHeight; + geometry.crop = crop; + geometry.pitch = AlignUp(geometry.width, PitchAlignment); + return geometry; +} + +void FillPictureInfo(const Geometry& geometry, const Decoder2OutputInfo& output, PictureInfo* picture) { + picture->isValid = 0; + picture->codecType = 0; + picture->frameWidth = 0; + picture->framePitch = 0; + picture->frameHeight = 0; + picture->isErrorPic = 0; + picture->ptsData = 0; + picture->attachedData = 0; + std::memset(picture->codec.reserved, 0, sizeof(picture->codec.reserved)); + if (!output.isValid) return; + picture->isValid = 1; + picture->frameWidth = geometry.width; + picture->framePitch = geometry.pitch; + picture->frameHeight = geometry.height; + picture->isErrorPic = output.isErrorFrame ? 1 : 0; + picture->codec.avc.frameCropLeftOffset = geometry.crop.left / 2; + picture->codec.avc.frameCropRightOffset = geometry.crop.right / 2; + picture->codec.avc.frameCropTopOffset = geometry.crop.top / 2; + picture->codec.avc.frameCropBottomOffset = geometry.crop.bottom / 2; +} + +int CopyPicture(const Decoder2OutputInfo& output, const Geometry& geometry, const FrameBuffer* frameBuffer) { + const auto bytes = static_cast(geometry.pitch) * geometry.height * 3 / 2; + if (frameBuffer->frameBuffer == nullptr) return VideodecErrorFrameBufferPointer; + if (frameBuffer->frameBufferSize < bytes) return VideodecErrorFrameBufferSize; + const auto sourcePitch = static_cast(output.framePitchInBytes); + const auto* source = static_cast(output.frameBuffer); + auto* destination = static_cast(frameBuffer->frameBuffer); + for (std::uint32_t row = 0; row < geometry.height; ++row) + std::memcpy(destination + static_cast(row) * geometry.pitch, source + static_cast(row) * sourcePitch, geometry.width); + const auto* sourceChroma = source + sourcePitch * geometry.height; + auto* destinationChroma = destination + static_cast(geometry.pitch) * geometry.height; + for (std::uint32_t row = 0; row < geometry.height / 2; ++row) + std::memcpy(destinationChroma + static_cast(row) * geometry.pitch, sourceChroma + static_cast(row) * sourcePitch, geometry.width); + return 0; +} + +} + extern "C" { -int APS5_VABI sceVideodecCreateDecoder(const void* pCfgInfoIn, const void* pRsrcInfoIn, void* pCtrlOut) { +int APS5_VABI sceVideodecCreateDecoder(const ConfigInfo* config, const ResourceInfo* resource, Ctrl* ctrl) { + if (config == nullptr || resource == nullptr || ctrl == nullptr) return VideodecErrorArgumentPointer; + if (config->thisSize < sizeof(ConfigInfo) || resource->thisSize < sizeof(ResourceInfo)) return VideodecErrorStructSize; + Decoder2ConfigInfo decoderConfig{}; + decoderConfig.thisSize = sizeof(Decoder2ConfigInfo); + decoderConfig.resourceType = 1; + decoderConfig.codecType = CodecAvc; + decoderConfig.profile = config->profile; + decoderConfig.maxLevel = config->maxLevel; + decoderConfig.maxFrameWidth = config->maxFrameWidth; + decoderConfig.maxFrameHeight = config->maxFrameHeight; + decoderConfig.maxDpbFrameCount = config->maxDpbFrameCount; + decoderConfig.decodePipelineDepth = 1; + Decoder2MemoryInfo decoderMemory{}; + decoderMemory.thisSize = sizeof(Decoder2MemoryInfo); + decoderMemory.cpuMemorySize = resource->cpuMemorySize; + decoderMemory.cpuMemory = resource->cpuMemory; + decoderMemory.cpuGpuMemorySize = resource->cpuGpuMemorySize; + decoderMemory.cpuGpuMemory = resource->cpuGpuMemory; + decoderMemory.maxFrameBufferSize = resource->maxFrameBufferSize; + decoderMemory.frameBufferAlignment = resource->frameBufferAlignment; + try { + sceVideodec2QueryDecoderMemoryInfo_nid_postfix(&decoderConfig, &decoderMemory); + } catch (const std::exception&) { + return VideodecErrorConfigInfo; + } + std::uint64_t handle = 0; + if (sceVideodec2CreateDecoder_nid_postfix(&decoderConfig, &decoderMemory, &handle) != 0) return VideodecErrorApiFail; + auto* decoder = new Decoder(); + decoder->handle = handle; + decoder->picture.resize(decoderMemory.maxFrameBufferSize); + { + std::lock_guard lock(decodersMutex); + decoders.emplace(decoder, decoder); + } + ctrl->thisSize = sizeof(Ctrl); + ctrl->handle = decoder; + ctrl->version = 1; + return 0; +} + +int APS5_VABI sceVideodecDecode(Ctrl* ctrl, const InputData* input, FrameBuffer* frameBuffer, PictureInfo* picture) { + if (ctrl == nullptr || input == nullptr || picture == nullptr) return VideodecErrorArgumentPointer; + if (frameBuffer == nullptr) return VideodecErrorFrameBufferPointer; + if (ctrl->thisSize < sizeof(Ctrl)) return VideodecErrorStructSize; + if (input->thisSize < sizeof(InputData)) return VideodecErrorStructSize; + if (picture->thisSize < sizeof(PictureInfo)) return VideodecErrorStructSize; + if (frameBuffer->thisSize < sizeof(FrameBuffer)) return VideodecErrorStructSize; + auto* decoder = FindDecoder(ctrl->handle); + if (decoder == nullptr) return VideodecErrorHandle; + if (input->auData == nullptr) return VideodecErrorAuPointer; + if (input->auSize == 0) return VideodecErrorAuSize; + if (frameBuffer->frameBuffer == nullptr) return VideodecErrorFrameBufferPointer; + Decoder2FrameBuffer target{}; + target.thisSize = sizeof(Decoder2FrameBuffer); + target.frameBuffer = decoder->picture.data(); + target.frameBufferSize = decoder->picture.size(); + Decoder2OutputInfo output{sizeof(Decoder2OutputInfo)}; + try { + sceVideodec2Decode_nid_postfix(decoder->handle, input, &target, &output); + } catch (const std::exception&) { + return VideodecErrorDecodeAu; + } + Decoder2Crop crop{}; + if (output.isValid) Videodec2PictureCrop_nid_no_patch(decoder->handle, output.frameBuffer, &crop); + const auto geometry = Measure(output, crop); + Decoder2AvcPictureInfo avc{sizeof(Decoder2AvcPictureInfo)}; + if (output.isValid) sceVideodec2GetAvcPictureInfo_nid_postfix(&output, &avc, nullptr); + FillPictureInfo(geometry, output, picture); + picture->ptsData = avc.ptsData; + picture->attachedData = avc.attachedData; + if (!output.isValid) return 0; + return CopyPicture(output, geometry, frameBuffer); +} + +int APS5_VABI sceVideodecDeleteDecoder(Ctrl* ctrl) { + if (ctrl == nullptr) return VideodecErrorArgumentPointer; + if (ctrl->thisSize < sizeof(Ctrl)) return VideodecErrorStructSize; + Decoder* decoder = nullptr; + { + std::lock_guard lock(decodersMutex); + const auto found = decoders.find(ctrl->handle); + if (found == decoders.end()) return VideodecErrorHandle; + decoder = found->second; + decoders.erase(found); + } + const auto handle = decoder->handle; + delete decoder; + try { + sceVideodec2DeleteDecoder_nid_postfix(handle); + } catch (const std::exception&) { + return VideodecErrorApiFail; + } + return 0; +} + +int APS5_VABI sceVideodecFlush(Ctrl* ctrl, FrameBuffer* frameBuffer, PictureInfo* picture) { + if (ctrl == nullptr || picture == nullptr) return VideodecErrorArgumentPointer; + if (frameBuffer == nullptr) return VideodecErrorFrameBufferPointer; + if (ctrl->thisSize < sizeof(Ctrl)) return VideodecErrorStructSize; + if (picture->thisSize < sizeof(PictureInfo)) return VideodecErrorStructSize; + if (frameBuffer->thisSize < sizeof(FrameBuffer)) return VideodecErrorStructSize; + auto* decoder = FindDecoder(ctrl->handle); + if (decoder == nullptr) return VideodecErrorHandle; + if (frameBuffer->frameBuffer == nullptr) return VideodecErrorFrameBufferPointer; + Decoder2FrameBuffer target{}; + target.thisSize = sizeof(Decoder2FrameBuffer); + target.frameBuffer = decoder->picture.data(); + target.frameBufferSize = decoder->picture.size(); + Decoder2OutputInfo output{sizeof(Decoder2OutputInfo)}; + try { + sceVideodec2Flush_nid_postfix(decoder->handle, &target, &output); + } catch (const std::exception&) { + return VideodecErrorDecodeAu; + } + Decoder2Crop crop{}; + if (output.isValid) Videodec2PictureCrop_nid_no_patch(decoder->handle, output.frameBuffer, &crop); + const auto geometry = Measure(output, crop); + Decoder2AvcPictureInfo avc{sizeof(Decoder2AvcPictureInfo)}; + if (output.isValid) sceVideodec2GetAvcPictureInfo_nid_postfix(&output, &avc, nullptr); + FillPictureInfo(geometry, output, picture); + picture->ptsData = avc.ptsData; + picture->attachedData = avc.attachedData; + if (!output.isValid) return 0; + return CopyPicture(output, geometry, frameBuffer); +} + +int APS5_VABI sceVideodecReset(Ctrl* ctrl) { + if (ctrl == nullptr) return VideodecErrorArgumentPointer; + if (ctrl->thisSize < sizeof(Ctrl)) return VideodecErrorStructSize; + auto* decoder = FindDecoder(ctrl->handle); + if (decoder == nullptr) return VideodecErrorHandle; + try { + sceVideodec2Reset_nid_postfix(decoder->handle); + } catch (const std::exception&) { + return VideodecErrorApiFail; + } + return 0; +} +int APS5_VABI sceVideodecMapMemory(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceVideodecDecode(void* pCtrlIn, const void* pInputDataIn, void* pFrameBufferInOut, void* pPictureInfoOut) { - NotImplemented_nid_no_patch(__func__); - return 0; -} -int APS5_VABI sceVideodecDeleteDecoder(void* pCtrlIn) { - NotImplemented_nid_no_patch(__func__); - return 0; -} -int APS5_VABI sceVideodecFlush(void* pCtrlIn, void* pFrameBufferInOut, void* pPictureInfoOut) { - NotImplemented_nid_no_patch(__func__); - return 0; -} -int APS5_VABI sceVideodecReset(void* pCtrlIn) { + +int APS5_VABI sceVideodecQueryResourceInfo(void) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceVideodec2/CMakeLists.txt b/core/libs/prx/libSceVideodec2/CMakeLists.txt index c2b41cd27b..8bc94af3d1 100644 --- a/core/libs/prx/libSceVideodec2/CMakeLists.txt +++ b/core/libs/prx/libSceVideodec2/CMakeLists.txt @@ -1,5 +1,6 @@ add_library(libSceVideodec2 SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Unimplemented.cpp ) target_include_directories(libSceVideodec2 PRIVATE ${LIBS_INCLUDE_DIR}) @@ -17,4 +18,14 @@ if(BUILD_TESTING) set_tests_properties(guest_videodec2 PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + + add_test_executable(guest_videodec2_unimplemented_tests ${LIBS_INCLUDE_DIR}/tests/GuestVideodec2Unimplemented.cpp) + target_include_directories(guest_videodec2_unimplemented_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_videodec2_unimplemented_tests PRIVATE libSceVideodec2 libc) + configure_windows_unwind(guest_videodec2_unimplemented_tests) + add_test(NAME guest_videodec2_unimplemented COMMAND guest_videodec2_unimplemented_tests) + if(WIN32) + set_tests_properties(guest_videodec2_unimplemented PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/prx/libSceVideodec2/Export.cpp b/core/libs/prx/libSceVideodec2/Export.cpp index 2f46ac5aa7..db8822b8eb 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; } @@ -624,7 +650,7 @@ int APS5_VABI sceVideodec2Decode_nid_postfix(std::uint64_t handle, const InputDa const std::unique_ptr packetOwner(packet, freePacket); if (decoder.parameterSets.size() + unit->size() > static_cast(std::numeric_limits::max())) throw std::runtime_error("Videodec2: invalid access unit"); if (av_new_packet(packet, static_cast(decoder.parameterSets.size() + unit->size())) < 0) throw std::runtime_error("Videodec2: cannot allocate packet data"); - std::memcpy(packet->data, decoder.parameterSets.data(), decoder.parameterSets.size()); + if (!decoder.parameterSets.empty()) std::memcpy(packet->data, decoder.parameterSets.data(), decoder.parameterSets.size()); std::memcpy(packet->data + decoder.parameterSets.size(), unit->data(), unit->size()); decoder.parameterSets.clear(); packet->pts = decoder.nextKey; @@ -683,7 +709,15 @@ int APS5_VABI sceVideodec2GetPictureInfo_nid_postfix(const OutputInfo* output, A if (owner() != handle) continue; const auto found = decoder->pictures.find(output->frameBuffer); if (found == decoder->pictures.end()) throw std::runtime_error("Videodec2: owned picture is missing"); - if (decoder->codec != CodecAvc) throw std::runtime_error("Videodec2: picture info is only implemented for H.264"); + if (decoder->codec == CodecVp9) { + auto* info = reinterpret_cast(first); + if (!Fits(info, sizeof(Vp9PictureInfo))) throw std::runtime_error("Videodec2: VP9 picture structure is too small"); + const auto size = info->thisSize; + *info = found->second.vp9; + info->thisSize = size; + return 0; + } + if (decoder->codec != CodecAvc) throw std::runtime_error("Videodec2: picture info is only implemented for H.264 and VP9"); const auto& picture = found->second; first->isValid = true; first->ptsData = picture.pts; diff --git a/core/libs/prx/libSceVideodec2/Unimplemented.cpp b/core/libs/prx/libSceVideodec2/Unimplemented.cpp new file mode 100644 index 0000000000..1cad37fd28 --- /dev/null +++ b/core/libs/prx/libSceVideodec2/Unimplemented.cpp @@ -0,0 +1,58 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceVideodec2TermLibrary() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceVideodec2QueryLibraryInfo() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceVideodec2QueryDecoderInfo() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceVideodec2MapMemory() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceVideodec2MapDirectMemory(void*, const void*) { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceVideodec2PresetDecoderConfigInfo() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceVideodec2PresetDecoderConfigInfoAvc() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceVideodec2PresetDecoderConfigInfoHevc() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceVideodec2PresetDecoderConfigInfoVp9() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceVideodec2CreateDecoderBid() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceVideodec2CreateHevcDecoder() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceVideodec2QueryHevcDecoderMemoryInfo() { + NotImplemented_nid_no_patch(__func__); +} + +int APS5_VABI sceVideodec2GetHevcPictureInfo() { + NotImplemented_nid_no_patch(__func__); +} + +} diff --git a/core/libs/prx/libSceVoice/Export.cpp b/core/libs/prx/libSceVoice/Export.cpp index bcc56dcb98..b2ae0e88ac 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); @@ -263,4 +280,68 @@ int APS5_VABI sceVoiceDisableChat(void) { return 0; } +int APS5_VABI sceVoiceInitHQ(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceGetMemorySize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceDeviceInCustomerData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceQAEnableRecording(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceVADAdjustment(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceVADPVTAdjustment(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceVADSetVersion(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceSetMuteFlagAll(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceSetBitRate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceUpdatePort(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoicePausePortAll(void) { + auto& voice = State(); + std::lock_guard lock(voice.mutex); + if (!voice.initialized) return VoiceErrorNotInitialized; + return 0; +} + +int APS5_VABI sceVoiceResumePortAll(void) { + auto& voice = State(); + std::lock_guard lock(voice.mutex); + if (!voice.initialized) return VoiceErrorNotInitialized; + return 0; +} + } diff --git a/core/libs/prx/libSceVoiceQoS/Export.cpp b/core/libs/prx/libSceVoiceQoS/Export.cpp index d8dc8fe238..96053a9e69 100644 --- a/core/libs/prx/libSceVoiceQoS/Export.cpp +++ b/core/libs/prx/libSceVoiceQoS/Export.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -68,6 +69,22 @@ int SetAttribute(std::map& endpoints, std::int32_t id, s return 0; } +int GetAttribute(std::map& endpoints, std::int32_t id, std::int32_t attribute, void* value, std::int32_t size) { + const auto endpoint = endpoints.find(id); + if (endpoint == endpoints.end() || !value || size <= 0) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; + const auto attr = endpoint->second.find(attribute); + if (attr != endpoint->second.end() && !attr->second.empty()) { + const auto toCopy = std::min(static_cast(size), attr->second.size()); + std::memcpy(value, attr->second.data(), toCopy); + if (static_cast(size) > toCopy) { + std::memset(static_cast(value) + toCopy, 0, static_cast(size) - toCopy); + } + } else { + std::memset(value, 0, static_cast(size)); + } + return 0; +} + template int Locked(Body&& body) { auto& qos = State(); @@ -144,17 +161,98 @@ int APS5_VABI sceVoiceQoSSetRemoteEndpointAttribute(int32_t remoteId, int32_t at return Locked([&](QoS& qos) { return SetAttribute(qos.remotes, remoteId, attributeId, value, size); }); } -int APS5_VABI sceVoiceQoSReadPacket() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVoiceQoSReadPacket(int32_t connectionId, void* buffer, uint32_t* size) { + return Locked([&](QoS& qos) { + if (!buffer || !size || !qos.connections.contains(connectionId)) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; + *size = 0; + return 0; + }); } -int APS5_VABI sceVoiceQoSWritePacket() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVoiceQoSWritePacket(int32_t connectionId, const void* buffer, uint32_t* size) { + return Locked([&](QoS& qos) { + if (!buffer || !size || !qos.connections.contains(connectionId)) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; + return 0; + }); } -int APS5_VABI sceVoiceQoSGetLocalEndpointAttribute() { +int APS5_VABI sceVoiceQoSGetLocalEndpointAttribute(int32_t localId, int32_t attributeId, void* value, int32_t size) { + return Locked([&](QoS& qos) { return GetAttribute(qos.locals, localId, attributeId, value, size); }); +} + +int APS5_VABI sceVoiceQoSGetRemoteEndpointAttribute(int32_t remoteId, int32_t attributeId, const void* value, int32_t size) { + (void)remoteId; + (void)attributeId; + (void)value; + (void)size; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceQoSGetConnectionAttribute(int32_t connectionId, int32_t attributeId, void* value, int32_t size) { + (void)connectionId; + (void)attributeId; + (void)value; + (void)size; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceQoSGetLocalEndpoint(int32_t connectionId, int32_t* localId) { + return Locked([&](QoS& qos) { + if (!localId) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; + const auto it = qos.connections.find(connectionId); + if (it == qos.connections.end()) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; + *localId = it->second.local; + return 0; + }); +} + +int APS5_VABI sceVoiceQoSGetRemoteEndpoint(int32_t connectionId, int32_t* remoteId) { + return Locked([&](QoS& qos) { + if (!remoteId) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; + const auto it = qos.connections.find(connectionId); + if (it == qos.connections.end()) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; + *remoteId = it->second.remote; + return 0; + }); +} + +int APS5_VABI sceVoiceQoSEnableChat() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceQoSDisableChat() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceQoSGetStatus(int32_t connectionId, int32_t statusId, void* value, int32_t size) { + (void)connectionId; + (void)statusId; + (void)value; + (void)size; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceQoSDebugGetStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceQoSInitHQ() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceQoSSetMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceQoSTerminate() { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceVrSetupDialog/Export.cpp b/core/libs/prx/libSceVrSetupDialog/Export.cpp index 128cbcd16a..f4b2718a30 100644 --- a/core/libs/prx/libSceVrSetupDialog/Export.cpp +++ b/core/libs/prx/libSceVrSetupDialog/Export.cpp @@ -43,6 +43,10 @@ int APS5_VABI sceVrSetupDialogUpdateStatus(void) { return g_status.load(); } +int APS5_VABI sceVrSetupDialogGetStatus(void) { + return g_status.load(); +} + int APS5_VABI sceVrSetupDialogGetResult(void* result) { const int status = g_status.load(); if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; @@ -55,6 +59,7 @@ int APS5_VABI sceVrSetupDialogGetResult(void* result) { int APS5_VABI sceVrSetupDialogClose(void) { if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } diff --git a/core/libs/prx/libSceVrTracker2/CMakeLists.txt b/core/libs/prx/libSceVrTracker2/CMakeLists.txt new file mode 100644 index 0000000000..aaf8e867d1 --- /dev/null +++ b/core/libs/prx/libSceVrTracker2/CMakeLists.txt @@ -0,0 +1,16 @@ +add_library(libSceVrTracker2 SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceVrTracker2 PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceVrTracker2 PRIVATE libc) +configure_windows_unwind(libSceVrTracker2) + +if(BUILD_TESTING) + add_test_executable(guest_vr_tracker2_tests ${LIBS_INCLUDE_DIR}/tests/GuestVrTracker2.cpp) + target_include_directories(guest_vr_tracker2_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_vr_tracker2_tests PRIVATE libSceVrTracker2 libc) + configure_windows_unwind(guest_vr_tracker2_tests) + add_test(NAME guest_vr_tracker2 COMMAND guest_vr_tracker2_tests) + if(WIN32) + set_tests_properties(guest_vr_tracker2 PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceVrTracker2/Export.cpp b/core/libs/prx/libSceVrTracker2/Export.cpp index 9e3639df41..e323b3c719 100644 --- a/core/libs/prx/libSceVrTracker2/Export.cpp +++ b/core/libs/prx/libSceVrTracker2/Export.cpp @@ -1,67 +1,260 @@ -#include #include -#include "SceTypes.hpp" +#include +#include +#include +#include #include "prx/libc/include/General.hpp" +namespace { + +constexpr std::int32_t SCE_OK = 0; +constexpr std::int32_t VR_TRACKER2_ERROR_NOT_INITIALIZED = static_cast(0x8A740001u); +constexpr std::int32_t VR_TRACKER2_ERROR_INVALID_ARGUMENT = static_cast(0x8A740006u); + +std::int32_t ClearThenReportNotInitialized(void* output, std::size_t size) { + if (output == nullptr) return VR_TRACKER2_ERROR_INVALID_ARGUMENT; + std::memset(output, 0, size); + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +} + extern "C" { -int APS5_VABI sceVrTracker2Finalize() { +std::int32_t APS5_VABI sceVrTracker2Finalize() { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +std::int32_t APS5_VABI sceVrTracker2GetCoordinateSystem() { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +std::int32_t APS5_VABI sceVrTracker2GetPlayAreaBoundaryGeometry() { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +std::int32_t APS5_VABI sceVrTracker2GetPlayAreaOrientedBoundingBox() { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +std::int32_t APS5_VABI sceVrTracker2GetResult() { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +std::int32_t APS5_VABI sceVrTracker2LocateCoordinateSystem() { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +std::int32_t APS5_VABI sceVrTracker2RegisterDevice() { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +std::int32_t APS5_VABI sceVrTracker2ResetLocalCoordinate() { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +std::int32_t APS5_VABI sceVrTracker2SetCoordinateSystem() { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +std::int32_t APS5_VABI sceVrTracker2UnregisterDevice() { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +std::int32_t APS5_VABI sceVrTracker2QueryMemory(const void* parameter, void* requirement) { + if (parameter == nullptr || requirement == nullptr) return VR_TRACKER2_ERROR_INVALID_ARGUMENT; + const auto* bytes = static_cast(parameter); + std::uint32_t size = 0; + std::memcpy(&size, bytes, sizeof size); + if (size != 0x20) return VR_TRACKER2_ERROR_INVALID_ARGUMENT; + for (std::size_t offset = sizeof size; offset < 0x20; ++offset) + if (bytes[offset] != 0) return VR_TRACKER2_ERROR_INVALID_ARGUMENT; + const std::uint32_t fields[3] = {0x20, 32u << 20, 64u << 10}; + std::memset(requirement, 0, 0x20); + std::memcpy(requirement, fields, sizeof fields); + return SCE_OK; +} + +std::int32_t APS5_VABI sceVrTracker2Initialize() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceVrTracker2GetCoordinateSystem() { - NotImplemented_nid_no_patch(__func__); +APS5_EXPORT("1m6DD9BBmSE", VrTracker2NotInitialized); +APS5_EXPORT("SgunslIO-C0", VrTracker2NotInitialized); +APS5_EXPORT("KMIToVnHrhc", VrTracker2NotInitialized); +APS5_EXPORT("XfEBVemeJyE", VrTracker2NotInitialized); +APS5_EXPORT("y79S3-zrJKg", VrTracker2NotInitialized); +APS5_EXPORT("b777Kpfe2J8", VrTracker2NotInitialized); +APS5_EXPORT("UfUYgiT7VK0", VrTracker2NotInitialized); +APS5_EXPORT("MB7B6396K6U", VrTracker2NotInitialized); +APS5_EXPORT("H2Mnx7gz4ac", VrTracker2NotInitialized); +APS5_EXPORT("SgNHzN6XaEc", VrTracker2NotInitialized); +APS5_EXPORT("sYBNLrMxjAM", VrTracker2NotInitialized); +APS5_EXPORT("3gQncidodmw", VrTracker2NotInitialized); +APS5_EXPORT("qnCmEbUGqWY", VrTracker2NotInitialized); +APS5_EXPORT("oGNn7JIdMHw", VrTracker2NotInitialized); +APS5_EXPORT("80IxOn8swIA", VrTracker2NotInitialized); +APS5_EXPORT("NTbJNAZA4ro", VrTracker2NotInitialized); +APS5_EXPORT("914+9oJxNts", VrTracker2NotInitialized); +APS5_EXPORT("c6SZtdEjH0E", VrTracker2NotInitialized); +APS5_EXPORT("74IXIMe+Lto", VrTracker2NotInitialized); +APS5_EXPORT("hmahOYMiYNY", VrTracker2NotInitialized); +APS5_EXPORT("pNXc6ZlLq-8", VrTracker2NotInitialized); +APS5_EXPORT("omJ7b8FqAN0", VrTracker2NotInitialized); +APS5_EXPORT("G2PWj1HLqn8", VrTracker2NotInitialized); +APS5_EXPORT("73zcxAFawYk", VrTracker2NotInitialized); +APS5_EXPORT("vegrWFNiQwc", VrTracker2NotInitialized); +APS5_EXPORT("6r01IawmypA", VrTracker2NotInitialized); +APS5_EXPORT("zeof7sF2fqE", VrTracker2NotInitialized); +APS5_EXPORT("Z0rfHOHQV1w", VrTracker2NotInitialized); +APS5_EXPORT("4l7x+uy3dWw", VrTracker2NotInitialized); +APS5_EXPORT("ZXlt-Xb3da4", VrTracker2NotInitialized); +APS5_EXPORT("BSwAdB3GM3o", VrTracker2NotInitialized); +APS5_EXPORT("9ATuhVfn9Dw", VrTracker2NotInitialized); +APS5_EXPORT("wjmVbmf58rE", VrTracker2NotInitialized); +APS5_EXPORT("4+B+TcHEHks", VrTracker2NotInitialized); +APS5_EXPORT("cjDz4kYJKTw", VrTracker2NotInitialized); +APS5_EXPORT("BjVg0fKV9VA", VrTracker2NotInitialized); +APS5_EXPORT("oDV5j-2Luic", VrTracker2NotInitialized); +APS5_EXPORT("t+86o9sCZ-Y", VrTracker2NotInitialized); +APS5_EXPORT("TIpYP35JzUs", VrTracker2NotInitialized); +APS5_EXPORT("LrSc+KuxIkg", VrTracker2NotInitialized); +APS5_EXPORT("Ur+M5wy+QBY", VrTracker2NotInitialized); +APS5_EXPORT("OvKC351Udqo", VrTracker2NotInitialized); +APS5_EXPORT("LQV4nz6wteY", VrTracker2NotInitialized); +APS5_EXPORT("x0pGthEwf88", VrTracker2NotInitialized); +APS5_EXPORT("d5Fjs0rHkBg", VrTracker2NotInitialized); +APS5_EXPORT("Yed3ZONcuMk", VrTracker2NotInitialized); +APS5_EXPORT("fkptW6uRdWc", VrTracker2NotInitialized); +APS5_EXPORT("HDlD-xE1Xuk", VrTracker2NotInitialized); +APS5_EXPORT("IGdE6nsMTAY", VrTracker2NotInitialized); +APS5_EXPORT("DSTergmOvvE", VrTracker2NotInitialized); +APS5_EXPORT("B7hPwvvg79E", VrTracker2NotInitialized); +APS5_EXPORT("ZwnIsDe1eNE", VrTracker2NotInitialized); +APS5_EXPORT("B2vw+s-0Dfo", VrTracker2NotInitialized); +APS5_EXPORT("4LNTLivfRuA", VrTracker2NotInitialized); +APS5_EXPORT("edajhZhz9Uo", VrTracker2NotInitialized); +APS5_EXPORT("QS+7oqiszuc", VrTracker2NotInitialized); +APS5_EXPORT("-dDyvzd2jf0", VrTracker2NotInitialized); +APS5_EXPORT("k8JDLkCrOgs", VrTracker2NotInitialized); +APS5_EXPORT("-HFx+dnDSOI", VrTracker2NotInitialized); +APS5_EXPORT("wgDeDXirl0g", VrTracker2NotInitialized); +APS5_EXPORT("a1kWSTYGBxg", VrTracker2NotInitialized); +APS5_EXPORT("zUwxBEKr3fc", VrTracker2NotInitialized); +APS5_EXPORT("TC7VTVzj0xc", VrTracker2NotInitialized); +APS5_EXPORT("1DwROxtuANs", VrTracker2NotInitialized); +APS5_EXPORT("wwbt4s8cbMQ", VrTracker2NotInitialized); +APS5_EXPORT("eGxBJ0IolOA", VrTracker2NotInitialized); +APS5_EXPORT("9BaFhspHF+E", VrTracker2NotInitialized); +APS5_EXPORT("lb9q5nJ4vzk", VrTracker2NotInitialized); +APS5_EXPORT("ZsD1-thjXS4", VrTracker2NotInitialized); +APS5_EXPORT("QdHQnM0XICI", VrTracker2NotInitialized); +APS5_EXPORT("Ek4WMOvwv6g", VrTracker2NotInitialized); +APS5_EXPORT("W1YAO6G+hNc", VrTracker2NotInitialized); +APS5_EXPORT("HAJB7irjl+0", VrTracker2NotInitialized); +APS5_EXPORT("x2mN50nKScs", VrTracker2NotInitialized); +APS5_EXPORT("cCKExge5d3A", VrTracker2NotInitialized); +APS5_EXPORT("gn6e5I4g9bw", VrTracker2NotInitialized); +APS5_EXPORT("OYTKt9diFOY", VrTracker2NotInitialized); +APS5_EXPORT("loU5lT92Zrw", VrTracker2NotInitialized); +APS5_EXPORT("o6fsgA3R2Yc", VrTracker2NotInitialized); +APS5_EXPORT("AeBtEIE0idk", VrTracker2NotInitialized); +APS5_EXPORT("DBa3wov7dKQ", VrTracker2NotInitialized); +APS5_EXPORT("ojTGClQ52r4", VrTracker2NotInitialized); +APS5_EXPORT("oOLE62H66z0", VrTracker2NotInitialized); +APS5_EXPORT("MHekhnRBKAw", VrTracker2NotInitialized); +APS5_EXPORT("JnSmj+5E8WY", VrTracker2NotInitialized); +APS5_EXPORT("N1P+melAgb8", VrTracker2NotInitialized); +APS5_EXPORT("-e7A3rAM3Yk", VrTracker2NotInitialized); +APS5_EXPORT("RefT1spBleg", VrTracker2NotInitialized); +APS5_EXPORT("DsOfryhbm4U", VrTracker2NotInitialized); +std::int32_t APS5_VABI VrTracker2NotInitialized() { + return VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("XknY0IDcOwU", VrTracker2ClearWideResult); +std::int32_t APS5_VABI VrTracker2ClearWideResult(void* result) { + return ClearThenReportNotInitialized(result, 24); +} + +APS5_EXPORT("ark13GkiskA", VrTracker2ClearNarrowResult); +std::int32_t APS5_VABI VrTracker2ClearNarrowResult(void* result) { + return ClearThenReportNotInitialized(result, 16); +} + +APS5_EXPORT("-YvjOUph8B0", VrTracker2CheckSecondThenReportNotInitialized); +std::int32_t APS5_VABI VrTracker2CheckSecondThenReportNotInitialized(const void* first, const void* second) { + (void)first; + return second == nullptr ? VR_TRACKER2_ERROR_INVALID_ARGUMENT : VR_TRACKER2_ERROR_NOT_INITIALIZED; +} + +APS5_EXPORT("QyZvFyf-QEk", VrTracker2Accept); +APS5_EXPORT("fTYZOZepkqE", VrTracker2Accept); +std::int32_t APS5_VABI VrTracker2Accept() { + return SCE_OK; +} + +APS5_EXPORT("TZIUIk9id3c", VrTracker2FillDefaults); +std::int32_t APS5_VABI VrTracker2FillDefaults(void* output) { + if (output == nullptr) APS5_INVALID_ARG_EX; + auto* bytes = static_cast(output); + const std::uint32_t first = 0xC000; + const std::uint32_t second = 0; + const std::uint16_t third = 0x40; + std::memcpy(bytes + 4, &first, sizeof first); + std::memcpy(bytes + 8, &second, sizeof second); + std::memcpy(bytes + 12, &third, sizeof third); + std::memcpy(bytes + 14, &third, sizeof third); + return SCE_OK; +} + +APS5_EXPORT("IB0qcDVu8vU", VrTracker2ClearWord); +std::int32_t APS5_VABI VrTracker2ClearWord(void* output) { + if (output == nullptr) APS5_INVALID_ARG_EX; + std::memset(output, 0, sizeof(std::uint32_t)); + return SCE_OK; +} + +APS5_EXPORT("jRmuXjsknsI", VrTracker2Unknown01); +std::int32_t APS5_VABI VrTracker2Unknown01() { + NotImplemented_nid_no_patch("jRmuXjsknsI"); return 0; } -int APS5_VABI sceVrTracker2GetPlayAreaBoundaryGeometry() { - NotImplemented_nid_no_patch(__func__); +APS5_EXPORT("NBhWy+Gl0tY", VrTracker2Unknown02); +std::int32_t APS5_VABI VrTracker2Unknown02() { + NotImplemented_nid_no_patch("NBhWy+Gl0tY"); return 0; } -int APS5_VABI sceVrTracker2ResetLocalCoordinate() { - NotImplemented_nid_no_patch(__func__); +APS5_EXPORT("9R6W33Au4B4", VrTracker2Unknown03); +std::int32_t APS5_VABI VrTracker2Unknown03() { + NotImplemented_nid_no_patch("9R6W33Au4B4"); return 0; } -int APS5_VABI sceVrTracker2GetPlayAreaOrientedBoundingBox() { - NotImplemented_nid_no_patch(__func__); +APS5_EXPORT("O8ZkTFtvUL4", VrTracker2Unknown04); +std::int32_t APS5_VABI VrTracker2Unknown04() { + NotImplemented_nid_no_patch("O8ZkTFtvUL4"); return 0; } -int APS5_VABI sceVrTracker2GetResult() { - NotImplemented_nid_no_patch(__func__); +APS5_EXPORT("940WPg+WwIk", VrTracker2Unknown05); +std::int32_t APS5_VABI VrTracker2Unknown05() { + NotImplemented_nid_no_patch("940WPg+WwIk"); return 0; } -int APS5_VABI sceVrTracker2Initialize() { - NotImplemented_nid_no_patch(__func__); +APS5_EXPORT("oZDm1bynCaA", VrTracker2Unknown06); +std::int32_t APS5_VABI VrTracker2Unknown06() { + NotImplemented_nid_no_patch("oZDm1bynCaA"); return 0; } -int APS5_VABI sceVrTracker2LocateCoordinateSystem() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceVrTracker2QueryMemory() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceVrTracker2RegisterDevice() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceVrTracker2SetCoordinateSystem() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceVrTracker2UnregisterDevice() { - NotImplemented_nid_no_patch(__func__); +APS5_EXPORT("U1VYczwqukY", VrTracker2Unknown07); +std::int32_t APS5_VABI VrTracker2Unknown07() { + NotImplemented_nid_no_patch("U1VYczwqukY"); return 0; } diff --git a/core/libs/prx/libSceWebBrowserDialog/Export.cpp b/core/libs/prx/libSceWebBrowserDialog/Export.cpp index 01bf0684d8..8ccf318370 100644 --- a/core/libs/prx/libSceWebBrowserDialog/Export.cpp +++ b/core/libs/prx/libSceWebBrowserDialog/Export.cpp @@ -37,6 +37,7 @@ int APS5_VABI sceWebBrowserDialogTerminate(void) { int APS5_VABI sceWebBrowserDialogClose(void) { if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } @@ -86,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/libSceWkFontConfig/Export.cpp b/core/libs/prx/libSceWkFontConfig/Export.cpp index fb34206c21..08ae01f5ec 100644 --- a/core/libs/prx/libSceWkFontConfig/Export.cpp +++ b/core/libs/prx/libSceWkFontConfig/Export.cpp @@ -6,5 +6,29 @@ extern "C" { APS5_DUMMY_FUN +int APS5_VABI sceWkFontConfigGetAttr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceWkFontConfigGetFont(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceWkFontConfigGetFontFromFontName(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceWkFontConfigGetFontName(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceWkFontConfigGetFontPath(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index dba309d6a7..f06b05d6af 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -1,4 +1,17 @@ if(BUILD_TESTING) + if(WIN32) + add_test_executable(windows_mappings_release_tests ${LIBS_INCLUDE_DIR}/tests/WindowsMappingsRelease.cpp) + target_include_directories(windows_mappings_release_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(windows_mappings_release_tests PRIVATE libc) + configure_windows_unwind(windows_mappings_release_tests) + add_test(NAME windows_mappings_release COMMAND windows_mappings_release_tests) + set_tests_properties(windows_mappings_release PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + foreach(mode 0 1 2) + add_test(NAME windows_mappings_release_mismatch_${mode} COMMAND windows_mappings_release_tests ${mode}) + set_tests_properties(windows_mappings_release_mismatch_${mode} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endforeach() + endif() + add_test_executable(guest_binary_word_tests ${LIBS_INCLUDE_DIR}/tests/GuestBinaryWord.cpp) target_include_directories(guest_binary_word_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_binary_word_tests PRIVATE libc) @@ -30,24 +43,27 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/FailureReport.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/gcc/StackGuard.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/windows/CrashReport.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/windows/CallTrace.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/x86_64/CallTrace.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/linux/DlIteratePhdr.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/macos/GuestImages.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Strings.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/AllocatingStrings.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/WideConversion.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/BoundsChecking.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Multibyte.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/RuneLocale.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/StringExtensions.cpp ${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 ${CMAKE_CURRENT_SOURCE_DIR}/src/Backtrace.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/DinkumwareAtomics.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Time.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/TimeExtras.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/AtomicBuiltins.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/FileAndHeap.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/HostErrno.cpp @@ -66,6 +82,7 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/ApplicationProcessParameters.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/JumpBuffer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Formatting.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/ScanWideField.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/FormattingWide.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/ScanningWide.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/RuntimeSupport.cpp @@ -86,6 +103,9 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/exception/Exports.cpp ) +set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/src/MathExact.cpp + TARGET_DIRECTORY libc PROPERTIES COMPILE_OPTIONS "-frounding-math") + target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/BinaryWordIo.cpp) target_include_directories(libc PRIVATE @@ -94,6 +114,21 @@ target_include_directories(libc PRIVATE target_link_libraries(libc PRIVATE zarchive) +target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/FloatingPoint.cpp) + +if(BUILD_TESTING) + add_test_executable(guest_floating_point_tests ${LIBS_INCLUDE_DIR}/tests/GuestFloatingPoint.cpp) + target_include_directories(guest_floating_point_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_floating_point_tests PRIVATE libc) + configure_windows_unwind(guest_floating_point_tests) + add_test(NAME guest_floating_point COMMAND guest_floating_point_tests) + set_tests_properties(guest_floating_point PROPERTIES TIMEOUT 10) + if(WIN32) + set_tests_properties(guest_floating_point PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + if(WIN32) target_link_libraries(libc PRIVATE advapi32 ws2_32 iphlpapi) endif() @@ -107,6 +142,16 @@ if(BUILD_TESTING) set_tests_properties(guest_fopen_errors PROPERTIES TIMEOUT 20) endif() + add_test_executable(guest_index_tests ${LIBS_INCLUDE_DIR}/tests/GuestIndex.cpp) + target_include_directories(guest_index_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_index_tests PRIVATE libc ${CMAKE_DL_LIBS}) + configure_windows_unwind(guest_index_tests) + add_test(NAME guest_index COMMAND guest_index_tests $) + if(WIN32) + set_tests_properties(guest_index PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_allocating_strings_tests ${LIBS_INCLUDE_DIR}/tests/GuestAllocatingStrings.cpp) target_include_directories(guest_allocating_strings_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_allocating_strings_tests PRIVATE libc) @@ -169,6 +214,16 @@ if(BUILD_TESTING) set_tests_properties(guest_mktime PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_time_extras_tests ${LIBS_INCLUDE_DIR}/tests/GuestTimeExtras.cpp) + target_include_directories(guest_time_extras_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_time_extras_tests PRIVATE libc) + configure_windows_unwind(guest_time_extras_tests) + add_test(NAME guest_time_extras COMMAND guest_time_extras_tests) + set_tests_properties(guest_time_extras PROPERTIES TIMEOUT 10) + if(WIN32) + set_tests_properties(guest_time_extras PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(precise_wait_tests ${LIBS_INCLUDE_DIR}/tests/PreciseWait.cpp) target_include_directories(precise_wait_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(precise_wait_tests PRIVATE libc) @@ -253,6 +308,16 @@ if(BUILD_TESTING) set_tests_properties(guest_wide_conversion PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_bounds_checking_tests ${LIBS_INCLUDE_DIR}/tests/GuestBoundsChecking.cpp) + target_include_directories(guest_bounds_checking_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_bounds_checking_tests PRIVATE libc) + configure_windows_unwind(guest_bounds_checking_tests) + add_test(NAME guest_bounds_checking COMMAND guest_bounds_checking_tests guest_bounds_checking.tmp) + set_tests_properties(guest_bounds_checking PROPERTIES TIMEOUT 10) + if(WIN32) + set_tests_properties(guest_bounds_checking PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_float_builtins_tests ${LIBS_INCLUDE_DIR}/tests/GuestFloatBuiltins.cpp) target_include_directories(guest_float_builtins_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_float_builtins_tests PRIVATE libc) @@ -298,6 +363,24 @@ if(BUILD_TESTING) set_tests_properties(guest_integer_builtins PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_wide_stream_scan_tests ${LIBS_INCLUDE_DIR}/tests/GuestWideStreamScan.cpp) + target_include_directories(guest_wide_stream_scan_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_wide_stream_scan_tests PRIVATE libc) + configure_windows_unwind(guest_wide_stream_scan_tests) + add_test(NAME guest_wide_stream_scan COMMAND guest_wide_stream_scan_tests) + if(WIN32) + set_tests_properties(guest_wide_stream_scan 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) @@ -318,6 +401,18 @@ if(BUILD_TESTING) set_tests_properties(guest_std_execute_once PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_execute_once_concurrency_tests ${LIBS_INCLUDE_DIR}/tests/GuestExecuteOnceConcurrency.cpp) + target_include_directories(guest_execute_once_concurrency_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_execute_once_concurrency_tests PRIVATE libc) + configure_windows_unwind(guest_execute_once_concurrency_tests) + foreach(onceEntry IN ITEMS mangled plain) + add_test(NAME guest_execute_once_concurrency_${onceEntry} COMMAND guest_execute_once_concurrency_tests ${onceEntry}) + set_tests_properties(guest_execute_once_concurrency_${onceEntry} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_execute_once_concurrency_${onceEntry} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() add_test_executable(guest_math_exact_tests ${LIBS_INCLUDE_DIR}/tests/GuestMathExact.cpp) target_include_directories(guest_math_exact_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_math_exact_tests PRIVATE libc) @@ -327,6 +422,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) @@ -397,7 +501,7 @@ if(BUILD_TESTING) endif() add_test_executable(guest_directory_tests ${LIBS_INCLUDE_DIR}/tests/GuestDirectory.cpp) target_include_directories(guest_directory_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_directory_tests PRIVATE libc) + target_link_libraries(guest_directory_tests PRIVATE libc libkernel) configure_windows_unwind(guest_directory_tests) add_test(NAME guest_directory COMMAND guest_directory_tests) if(WIN32) @@ -557,17 +661,24 @@ if(BUILD_TESTING) set_tests_properties(guest_errors PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() - if(WIN32) + if(WIN32 OR CMAKE_SYSTEM_NAME STREQUAL "Linux") add_test_executable(guest_call_trace_tests ${LIBS_INCLUDE_DIR}/tests/GuestCallTrace.cpp) target_include_directories(guest_call_trace_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_call_trace_tests PRIVATE libc) configure_windows_unwind(guest_call_trace_tests) add_test(NAME guest_call_trace COMMAND guest_call_trace_tests) add_test(NAME guest_call_trace_abort COMMAND guest_call_trace_tests abort) - set_tests_properties(guest_call_trace guest_call_trace_abort PROPERTIES ENVIRONMENT_MODIFICATION - "PATH=path_list_prepend:$") + if(WIN32) + set_tests_properties(guest_call_trace guest_call_trace_abort PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() set_tests_properties(guest_call_trace_abort PROPERTIES TIMEOUT 20 PASS_REGULAR_EXPRESSION - "last traced system library calls[^~]*#1 thread [0-9]+ libTraced.prx Integers from [^~]*guest_call_trace_tests.exe[^~]*#2 thread [0-9]+ libOther.prx Blocks from[^~]*most called traced imports \\(2 calls in total\\):[^~]* 1 libTraced.prx Integers[^~]* 1 libOther.prx Blocks") + "last traced system library calls[^~]*#1 thread [0-9]+ libTraced.prx Integers from [^~]*guest_call_trace_tests${CMAKE_EXECUTABLE_SUFFIX}[^~]*#2 thread [0-9]+ libOther.prx Blocks from[^~]*last traced calls of thread [0-9]+:[^~]*#1 libTraced.prx Integers from[^~]*#2 libOther.prx Blocks from[^~]*most called traced imports \\(2 calls in total\\):[^~]* 1 libTraced.prx Integers[^~]* 1 libOther.prx Blocks") + if(NOT WIN32) + add_test(NAME guest_call_trace_abort_race COMMAND guest_call_trace_tests abort-race) + set_tests_properties(guest_call_trace_abort_race PROPERTIES TIMEOUT 20 PASS_REGULAR_EXPRESSION + "last traced system library calls[^~]*#102 thread [0-9]+ libTraced.prx Integers from[^~]*most called traced imports \\(102 calls in total\\):[^~]* 101 libTraced.prx Integers[^~]* 1 libOther.prx Blocks") + endif() endif() add_test_executable(guest_syslog_tests ${LIBS_INCLUDE_DIR}/tests/GuestSyslog.cpp) target_include_directories(guest_syslog_tests PRIVATE ${LIBS_INCLUDE_DIR}) @@ -685,6 +796,15 @@ if(BUILD_TESTING) set_tests_properties(guest_overflow_error PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_bad_optional_access_tests ${LIBS_INCLUDE_DIR}/tests/GuestBadOptionalAccess.cpp) + target_include_directories(guest_bad_optional_access_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_bad_optional_access_tests PRIVATE libc) + configure_windows_unwind(guest_bad_optional_access_tests) + add_test(NAME guest_bad_optional_access COMMAND guest_bad_optional_access_tests) + if(WIN32) + set_tests_properties(guest_bad_optional_access PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() if(UNIX) add_test_executable(exception_runtime_tests ${LIBS_INCLUDE_DIR}/tests/ExceptionRuntime.cpp) target_include_directories(exception_runtime_tests PRIVATE ${LIBS_INCLUDE_DIR}) @@ -751,6 +871,7 @@ if(BUILD_TESTING) add_dependencies(application_heap_tests guest_sdk_heap_fixture guest_sdk_heap_partial_fixture) add_test(NAME application_heap_guest_sdk COMMAND application_heap_tests sdk $) add_test(NAME application_heap_guest_sdk_partial COMMAND application_heap_tests sdk-partial $) + add_test(NAME application_heap_guest_sdk_default COMMAND application_heap_tests sdk-default $) endif() if(WIN32) set_tests_properties(application_heap_replacement application_heap_invalid_metadata application_heap_default application_heap_exit_order application_heap_scripting application_heap_lazy @@ -781,11 +902,18 @@ if(BUILD_TESTING) add_test_executable(cpu_topology_tests ${LIBS_INCLUDE_DIR}/tests/CpuTopology.cpp) target_include_directories(cpu_topology_tests PRIVATE ${LIBS_INCLUDE_DIR}) add_test(NAME cpu_topology COMMAND cpu_topology_tests) + endif() + if(CMAKE_SYSTEM_NAME STREQUAL "Linux" OR WIN32) add_test_executable(guest_backtrace_tests ${LIBS_INCLUDE_DIR}/tests/GuestBacktrace.cpp) target_include_directories(guest_backtrace_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_backtrace_tests PRIVATE libc) + configure_windows_unwind(guest_backtrace_tests) set_target_properties(guest_backtrace_tests PROPERTIES ENABLE_EXPORTS ON) add_test(NAME guest_backtrace COMMAND guest_backtrace_tests) + if(WIN32) + set_tests_properties(guest_backtrace PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() add_test_executable(log_macros_tests ${LIBS_INCLUDE_DIR}/tests/LogMacros.cpp) target_include_directories(log_macros_tests PRIVATE ${LIBS_INCLUDE_DIR}) @@ -798,7 +926,11 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") add_test_executable(thread_name_tests ${LIBS_INCLUDE_DIR}/tests/ThreadName.cpp) target_include_directories(thread_name_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(thread_name_tests PRIVATE libc) + configure_windows_unwind(thread_name_tests) add_test(NAME thread_name COMMAND thread_name_tests) + set_tests_properties(thread_name PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") endif() add_test_executable(guest_heap_report_for_game_tests ${LIBS_INCLUDE_DIR}/tests/GuestHeapReportForGame.cpp) @@ -887,6 +1019,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/CallTrace.hpp b/core/libs/prx/libc/include/CallTrace.hpp index 2c1f46c600..b53c1ba96e 100644 --- a/core/libs/prx/libc/include/CallTrace.hpp +++ b/core/libs/prx/libc/include/CallTrace.hpp @@ -21,6 +21,7 @@ struct CallTraceCount { extern "C" { void* TraceImport_nid_no_patch(void* target, const char* name, const char* library); std::size_t CopyCallTrace_nid_no_patch(CallTraceEntry* entries, std::size_t capacity); +std::size_t CopyThreadCallTrace_nid_no_patch(std::uint64_t thread, CallTraceEntry* entries, std::size_t capacity); std::size_t CopyCallCounts_nid_no_patch(CallTraceCount* entries, std::size_t capacity, std::uint64_t* total); } diff --git a/core/libs/prx/libc/include/CpuTopology.hpp b/core/libs/prx/libc/include/CpuTopology.hpp index a1061cb3c4..0b51816e23 100644 --- a/core/libs/prx/libc/include/CpuTopology.hpp +++ b/core/libs/prx/libc/include/CpuTopology.hpp @@ -54,6 +54,24 @@ inline std::uint64_t MaskFromCpuList(const char* path) { } #endif +inline Layout LayoutFromClasses(const std::uint64_t* byClass, const bool* seen, std::size_t count) { + Layout result{}; + int lowest = -1; + int highest = -1; + for (std::size_t cls = 0; cls < count; ++cls) { + if (!seen[cls]) continue; + if (lowest < 0) lowest = static_cast(cls); + highest = static_cast(cls); + } + if (lowest < 0 || lowest == highest) return result; + for (int cls = 0; cls < highest; ++cls) { + if (seen[cls]) result.efficient |= byClass[cls]; + } + result.performant = byClass[highest]; + result.hybrid = result.efficient != 0 && result.performant != 0; + return result; +} + inline const Layout& Get() { static const Layout layout = [] { Layout result{}; @@ -78,17 +96,10 @@ inline const Layout& Get() { } offset += entry->Size; } - int lowest = -1; - int highest = -1; - for (int cls = 0; cls < 256; ++cls) { - if (!seen[cls]) continue; - if (lowest < 0) lowest = cls; - highest = cls; - } - if (lowest < 0 || lowest == highest) return result; - result.efficient = byClass[lowest]; - result.performant = byClass[highest]; - result.hybrid = result.efficient != 0 && result.performant != 0; + const auto classified = LayoutFromClasses(byClass, seen, 256); + result.efficient = classified.efficient; + result.performant = classified.performant; + result.hybrid = classified.hybrid; #elif defined(__linux__) cpu_set_t set; CPU_ZERO(&set); diff --git a/core/libs/prx/libc/include/FileStream.hpp b/core/libs/prx/libc/include/FileStream.hpp index f5737cc3ce..1b9954d873 100644 --- a/core/libs/prx/libc/include/FileStream.hpp +++ b/core/libs/prx/libc/include/FileStream.hpp @@ -1,6 +1,7 @@ #ifndef CORE_LIBS_PRX_LIBC_INCLUDE_FILESTREAM_HPP #define CORE_LIBS_PRX_LIBC_INCLUDE_FILESTREAM_HPP +#include #include #include #include @@ -16,9 +17,19 @@ #include "prx/libc/include/FileLock.hpp" extern "C" void* HostStandardHandle_nid_no_patch(int descriptor); extern "C" void KeepStandardHandle_nid_no_patch(int descriptor, void* handle); +struct UcrtStreamState { + char* pointer; + char* base; + int count; + long flags; +}; #else #include #endif +#if defined(__GLIBC__) +#include +extern "C" int _IO_switch_to_get_mode(std::FILE* stream); +#endif extern "C" int DuplicateHostDescriptor_nid_no_patch(int descriptor); struct GuestFilePrefix { @@ -119,7 +130,8 @@ public: return randomDevice; } - bool Reopen(const std::filesystem::path& filename, const char* mode) { + bool Reopen(const std::filesystem::path& filename, const char* mode, + [[maybe_unused]] std::FILE* (*open)(const std::filesystem::path&, const std::string&) = nullptr) { auto* previous = GetHandle(); _guest = {}; guestError = false; @@ -130,10 +142,46 @@ public: const int replaced = previous ? _fileno(previous) : -1; auto* const standard = HostStandardHandle_nid_no_patch(replaced); GuestFileLockForget_nid_no_patch(replaced); - const int kept = IsHostStandard() ? DuplicateHostDescriptor_nid_no_patch(replaced) : -1; - if (kept >= 0) ::_lock_file(previous); - _handle = ::_wfreopen(filename.c_str(), wideMode.c_str(), previous); - if (kept >= 0) RestoreHostStandard(previous, kept); + if (open && !filename.empty()) { + if (!IsHostStandard()) { + _handle = nullptr; + std::fclose(previous); + _handle = open(filename, mode); + } else { + std::wstring resetMode; + for (const char character : std::string_view(mode)) + if (character != 'x') resetMode.push_back(static_cast(character)); + const int kept = DuplicateHostDescriptor_nid_no_patch(replaced); + if (kept >= 0) ::_lock_file(previous); + _handle = nullptr; + try { + std::FILE* const replacement = open(filename, mode); + if (replacement) { + if (::_wfreopen(L"NUL", resetMode.c_str(), previous) == previous) { + const int moved = ::_dup2(_fileno(replacement), replaced); + const int error = errno; + std::fclose(replacement); + if (moved == 0) _handle = previous; + else errno = error; + } else { + const int error = errno; + std::fclose(replacement); + errno = error; + } + } + } catch (...) { + if (kept >= 0) RestoreHostStandard(previous, kept); + KeepStandardHandle_nid_no_patch(replaced, standard); + throw; + } + if (kept >= 0) RestoreHostStandard(previous, kept); + } + } else { + const int kept = IsHostStandard() ? DuplicateHostDescriptor_nid_no_patch(replaced) : -1; + if (kept >= 0) ::_lock_file(previous); + _handle = ::_wfreopen(filename.c_str(), wideMode.c_str(), previous); + if (kept >= 0) RestoreHostStandard(previous, kept); + } KeepStandardHandle_nid_no_patch(replaced, standard); #else if (::fileno(previous) == 2) KeepHostStandardError_nid_no_patch(); @@ -175,6 +223,43 @@ public: (endOfFile ? 0x20 : 0) | (error || guestError ? 0x40 : 0)); } + bool SwitchToReading() { +#ifdef _WIN32 + ::_lock_file(GetHandle()); + const bool switched = SwitchToReadingUnlocked(); + ::_unlock_file(GetHandle()); + return switched; +#else + ::flockfile(GetHandle()); + const bool switched = SwitchToReadingUnlocked(); + ::funlockfile(GetHandle()); + return switched; +#endif + } + + bool SwitchToReadingUnlocked() { +#ifdef _WIN32 + constexpr long UcrtReading = 0x0001; + constexpr long UcrtWriting = 0x0002; + constexpr long UcrtUpdate = 0x0004; + auto* native = GetHandle(); + std::atomic_ref state(reinterpret_cast(native)->flags); + if ((state.load() & (UcrtReading | UcrtUpdate)) != UcrtUpdate) return true; + const bool switched = (state.load() & UcrtWriting) == 0 || ::_fflush_nolock(native) == 0; + if (switched) state.fetch_or(UcrtReading); + SyncUnlockedStatus(); + return switched; +#elif defined(__GLIBC__) + auto* native = GetHandle(); + if (!::__freadable(native) || !::__fwriting(native)) return true; + const bool switched = ::_IO_switch_to_get_mode(native) == 0; + SyncUnlockedStatus(); + return switched; +#else + return true; +#endif + } + void SetError() { guestError = true; SyncStatus(); 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/General.hpp b/core/libs/prx/libc/include/General.hpp index c60a1704fa..92bbc3caff 100644 --- a/core/libs/prx/libc/include/General.hpp +++ b/core/libs/prx/libc/include/General.hpp @@ -2,8 +2,10 @@ #define CORE_LIBS_PRX_LIBC_INCLUDE_GENERAL_HPP #include +#include #include #include +#include #include "general/LogMacros.hpp" #include "general/VabiMacros.hpp" @@ -12,11 +14,19 @@ extern "C" [[noreturn]] void NotImplemented_nid_no_patch(const char* funcName); extern "C" void CxaFinalize_nid_no_patch(void* dsoHandle); extern "C" bool StopAtPrefixLetter_nid_no_patch(const char* str, char** endptr, int base); +extern "C" bool RejectIntegerBase_nid_no_patch(const char* str, char** endptr, int base, bool setError); #ifndef _WIN32 extern "C" int KeepHostStandardError_nid_no_patch(); extern "C" int HostStandardError_nid_no_patch(); +extern "C" int KeepHostStandardOutput_nid_no_patch(); +#else +extern "C" bool AdoptHostStandardStream_nid_no_patch(int standard, int saved); +extern "C" std::FILE* HostStandardOutputFile_nid_no_patch(); +extern "C" std::FILE* HostStandardErrorFile_nid_no_patch(); #endif +extern "C" bool CopyGuestEnvironment_nid_no_patch(const char* name, std::string& value); + extern "C" std::filesystem::path ResolvePath_nid_no_patch(const char* path); extern "C" std::filesystem::path ResolvePathOrRelinkedModule_nid_no_patch(const char* path); extern "C" std::filesystem::path ResolveArchivedPath_nid_no_patch(const char* path, bool module = false); @@ -42,6 +52,13 @@ extern "C" void* HostStandardHandle_nid_no_patch(int descriptor); extern "C" void KeepStandardHandle_nid_no_patch(int descriptor, void* handle); #endif +using PthreadCreateFn_nid_no_patch = int (APS5_VABI*)(void**, const void*, void* (APS5_VABI*)(void*), void*); +using PthreadJoinFn_nid_no_patch = int (APS5_VABI*)(void*, void**); +using PthreadDetachFn_nid_no_patch = int (APS5_VABI*)(void*); +extern "C" void RegisterPthreadCreate_nid_no_patch(PthreadCreateFn_nid_no_patch fn); +extern "C" void RegisterPthreadJoin_nid_no_patch(PthreadJoinFn_nid_no_patch fn); +extern "C" void RegisterPthreadDetach_nid_no_patch(PthreadDetachFn_nid_no_patch fn); + #define APS5_INVALID_ARG_EX throw std::invalid_argument(std::string(__func__) + ": invalid argument") #define APS5_DUMMY_FUN \ diff --git a/core/libs/prx/libc/include/GuestArena.hpp b/core/libs/prx/libc/include/GuestArena.hpp index d5a00acdec..bf0d756d40 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); @@ -21,6 +27,7 @@ void* GuestArenaAllocate_nid_postfix(std::size_t bytes, std::size_t alignment); void* GuestArenaAllocateAtOrAbove_nid_postfix(std::uintptr_t hint, std::size_t bytes, std::size_t alignment); void* GuestArenaAllocateFromHint_nid_postfix(std::uintptr_t hint, std::size_t bytes, std::size_t alignment); std::uintptr_t GuestArenaHighestFree_nid_no_patch(std::size_t bytes, std::size_t alignment); +bool GuestArenaNextUsed_nid_no_patch(std::uintptr_t address, std::uintptr_t* start, std::uintptr_t* end); void GuestArenaMarkUsed_nid_postfix(const void* pointer, std::size_t bytes); void GuestArenaRelease_nid_postfix(const void* pointer, std::size_t bytes); // The reserved range, and whether it was reserved with page write watching (Windows MEM_WRITE_WATCH). @@ -49,6 +56,9 @@ void* GuestArenaMapAlias_nid_postfix(std::uintptr_t address, std::size_t bytes); void GuestArenaUnmapAlias_nid_postfix(void* alias); bool GuestArenaResidentSpan_nid_postfix(std::uintptr_t address, std::size_t bytes, std::uintptr_t* start, std::uintptr_t* end); void GuestArenaForgetSection_nid_postfix(void* section); +bool GuestArenaBeginHostImport_nid_no_patch(void* pointer, std::size_t bytes); +void GuestArenaEndHostImport_nid_no_patch(void* pointer, std::size_t bytes); +bool GuestArenaForgetReusableSection_nid_no_patch(void* section); #else void GuestArenaSetSharedBacking_nid_postfix(SharedBackingResolver resolver); void GuestArenaSetSharedBackingWriter_nid_no_patch(SharedBackingWriter writer); @@ -78,6 +88,23 @@ private: bool open; }; +#ifdef _WIN32 +class HostImportAccess { +public: + HostImportAccess(void* pointer, std::size_t bytes) : pointer(pointer), bytes(bytes), open(GuestArenaBeginHostImport_nid_no_patch(pointer, bytes)) {} + ~HostImportAccess() { + if (open) GuestArenaEndHostImport_nid_no_patch(pointer, bytes); + } + HostImportAccess(const HostImportAccess&) = delete; + HostImportAccess& operator=(const HostImportAccess&) = delete; + +private: + void* pointer; + std::size_t bytes; + bool open; +}; +#endif + } #endif 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/HostStandardStreams.hpp b/core/libs/prx/libc/include/HostStandardStreams.hpp new file mode 100644 index 0000000000..5c01f269cf --- /dev/null +++ b/core/libs/prx/libc/include/HostStandardStreams.hpp @@ -0,0 +1,28 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_HOSTSTANDARDSTREAMS_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_HOSTSTANDARDSTREAMS_HPP + +#if defined(__MINGW32__) && !defined(__USE_MINGW_ANSI_STDIO) +#define __USE_MINGW_ANSI_STDIO 1 +#endif +#ifdef __cplusplus +#include +#else +#include +#endif + +#ifdef _WIN32 +#undef stdout +#undef stderr +#ifdef __cplusplus +extern "C" { +#endif +FILE* HostStandardOutputFile_nid_no_patch(void); +FILE* HostStandardErrorFile_nid_no_patch(void); +#ifdef __cplusplus +} +#endif +#define stdout HostStandardOutputFile_nid_no_patch() +#define stderr HostStandardErrorFile_nid_no_patch() +#endif + +#endif diff --git a/core/libs/prx/libc/include/RemoveTree.hpp b/core/libs/prx/libc/include/RemoveTree.hpp new file mode 100644 index 0000000000..ec5429a0aa --- /dev/null +++ b/core/libs/prx/libc/include/RemoveTree.hpp @@ -0,0 +1,62 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_REMOVETREE_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_REMOVETREE_HPP + +#include +#include +#include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#endif + +inline void RemoveTree(const std::filesystem::path& target, std::error_code& error) { + error.clear(); +#ifdef _WIN32 + const std::wstring wide = target.wstring(); + const DWORD attributes = GetFileAttributesW(wide.c_str()); + if (attributes == INVALID_FILE_ATTRIBUTES) { + const DWORD missing = GetLastError(); + if (missing != ERROR_FILE_NOT_FOUND && missing != ERROR_PATH_NOT_FOUND) + error = std::error_code(static_cast(missing), std::system_category()); + return; + } + const bool directory = (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0; + if (directory && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0) { + std::error_code listing; + { + std::filesystem::directory_iterator entries(target, listing); + while (!listing && entries != std::filesystem::directory_iterator()) { + RemoveTree(entries->path(), error); + if (error) return; + entries.increment(listing); + } + } + if (listing) { + error = listing; + return; + } + } + if (directory ? RemoveDirectoryW(wide.c_str()) != 0 : DeleteFileW(wide.c_str()) != 0) return; + const DWORD failed = GetLastError(); + SetFileAttributesW(wide.c_str(), FILE_ATTRIBUTE_NORMAL); + if (directory ? RemoveDirectoryW(wide.c_str()) != 0 : DeleteFileW(wide.c_str()) != 0) return; + error = std::error_code(static_cast(failed), std::system_category()); +#else + std::error_code removal; + std::filesystem::remove_all(target, removal); + if (removal) error = removal; +#endif +} + +inline void RemoveTree(const std::filesystem::path& target) { + std::error_code error; + RemoveTree(target, error); + if (error) throw std::filesystem::filesystem_error("RemoveTree", target, error); +} + +#endif diff --git a/core/libs/prx/libc/include/RenameDot.hpp b/core/libs/prx/libc/include/RenameDot.hpp new file mode 100644 index 0000000000..4364a80d39 --- /dev/null +++ b/core/libs/prx/libc/include/RenameDot.hpp @@ -0,0 +1,28 @@ +#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; + const auto type = std::filesystem::status(resolved, status).type(); + if (type == std::filesystem::file_type::not_found) return status == std::errc::not_a_directory ? 20 : 2; + if (status) return FilesystemError(status); + return type == std::filesystem::file_type::directory ? 22 : 20; +} + +#endif diff --git a/core/libs/prx/libc/include/ScanWideField.hpp b/core/libs/prx/libc/include/ScanWideField.hpp new file mode 100644 index 0000000000..f210ec5d5e --- /dev/null +++ b/core/libs/prx/libc/include/ScanWideField.hpp @@ -0,0 +1,15 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_SCANWIDEFIELD_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_SCANWIDEFIELD_HPP + +#include +#include + +namespace LibcDetail { + +std::size_t ReadWideScanCharacter(const char* text, char32_t* point); +bool InWideScanset(const std::string& specifier, char32_t point); +void RequireNoPositionalWideScan(const char* format); + +} + +#endif diff --git a/core/libs/prx/libc/include/Shutdown.hpp b/core/libs/prx/libc/include/Shutdown.hpp index 8e0f9ec209..a59aa05ced 100644 --- a/core/libs/prx/libc/include/Shutdown.hpp +++ b/core/libs/prx/libc/include/Shutdown.hpp @@ -17,5 +17,6 @@ extern "C" [[noreturn]] void LibcExitWithoutHandlers_nid_no_patch(int code); extern "C" [[noreturn]] void LibcTerminate_nid_no_patch(int code); extern "C" void LibcSetEndFlush_nid_no_patch(void (*flush)()); extern "C" void LibcRunEndFlush_nid_no_patch(); +extern "C" void LibcSetExitDrain_nid_no_patch(void (*drain)()); #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/include/WindowsDescriptor.hpp b/core/libs/prx/libc/include/WindowsDescriptor.hpp index d8ac9209ca..66ebc1d9ac 100644 --- a/core/libs/prx/libc/include/WindowsDescriptor.hpp +++ b/core/libs/prx/libc/include/WindowsDescriptor.hpp @@ -28,6 +28,15 @@ static inline ACCESS_MASK WindowsHandleAccess_nid_no_patch(HANDLE handle, int de return information.GrantedAccess; } +static inline std::uint32_t WindowsFileNumber_nid_no_patch(std::uint64_t fileId) { + const std::uint32_t folded = static_cast(fileId) ^ (static_cast(fileId >> 32) * 0x9E3779B1u); + return folded == 0 ? 1 : folded; +} + +static inline std::uint32_t WindowsFileNumber_nid_no_patch(const BY_HANDLE_FILE_INFORMATION& identity) { + return WindowsFileNumber_nid_no_patch((static_cast(identity.nFileIndexHigh) << 32) | identity.nFileIndexLow); +} + static inline bool WindowsHandleOnlyAppends_nid_no_patch(ACCESS_MASK access) { return (access & (FILE_WRITE_DATA | FILE_APPEND_DATA)) == FILE_APPEND_DATA; } diff --git a/core/libs/prx/libc/include/WindowsFormatting.hpp b/core/libs/prx/libc/include/WindowsFormatting.hpp index c143d2a17e..0c8dd2861f 100644 --- a/core/libs/prx/libc/include/WindowsFormatting.hpp +++ b/core/libs/prx/libc/include/WindowsFormatting.hpp @@ -16,6 +16,7 @@ #include extern "C" int APS5_VABI strerror_r_nid_postfix(int error, char* buffer, std::size_t length); +extern "C" std::FILE* GuestStandardOutputFile_nid_no_patch(); namespace LibcDetail { @@ -413,7 +414,7 @@ inline int FormatWindows(char* buffer, size_t size, const char* format, const vo inline int PrintWindows(const char* format, const void* args) { std::string buffer; const int size = FormatWindows(nullptr, 0, format, args, &buffer); - if (std::fwrite(buffer.data(), 1, static_cast(size), stdout) != static_cast(size)) + if (std::fwrite(buffer.data(), 1, static_cast(size), GuestStandardOutputFile_nid_no_patch()) != static_cast(size)) throw std::runtime_error("Formatted output write failed"); return size; } diff --git a/core/libs/prx/libc/include/WindowsMappings.hpp b/core/libs/prx/libc/include/WindowsMappings.hpp index 4437020a26..624685e8a9 100644 --- a/core/libs/prx/libc/include/WindowsMappings.hpp +++ b/core/libs/prx/libc/include/WindowsMappings.hpp @@ -76,11 +76,12 @@ public: reset(reinterpret_cast(address), bytes); } - void Map(void* address, std::size_t bytes, HANDLE section, std::uint64_t offset, DWORD protection, bool watched = false, bool resident = false, DWORD viewProtection = PAGE_EXECUTE_READWRITE, std::size_t sectionBytes = SIZE_MAX) { + void Map(void* address, std::size_t bytes, HANDLE section, std::uint64_t offset, DWORD protection, bool watched = false, bool resident = false, DWORD viewProtection = PAGE_EXECUTE_READWRITE, std::size_t sectionBytes = SIZE_MAX, std::vector>* createdPrivate = nullptr) { if (sectionBytes < bytes && (resident || sectionBytes == 0 || bytes - sectionBytes >= pageBytes)) throw std::runtime_error("shared guest mapping extends a whole page past its section"); std::lock_guard lock(mutex); collectableRanges.clear(); auto cursor = reinterpret_cast(address); + if (createdPrivate != nullptr) createdPrivate->reserve(createdPrivate->size() + bytes / pageBytes); reset(cursor, bytes); if (resident) watchResident = watched; HANDLE duplicate = nullptr; @@ -111,6 +112,10 @@ public: while (done + runBytes < bytes && (base + runBytes) % chunkBytes != 0 && single(done + runBytes)) runBytes += pageBytes; split(base, runBytes); commitResident(base, runBytes); + if (createdPrivate != nullptr) { + if (!createdPrivate->empty() && createdPrivate->back().first + createdPrivate->back().second == base) createdPrivate->back().second += runBytes; + else createdPrivate->emplace_back(base, runBytes); + } residentRuns.emplace(base, Run{base + runBytes, {}, static_cast(runBytes >> 12)}); for (std::size_t at = 0; at < runBytes; at += pageBytes) { const auto slot = slotOf(done + at); @@ -187,11 +192,15 @@ public: const auto found = views.find(base); if (found == views.end() || !writable(found->second.protection)) return false; auto& view = found->second; - if (view.resident) { + const auto written = subpagesOf(base, address, address + 1); + if (view.resident && (view.armed & written) == 0) { const auto memory = query(address); return memory.State == MEM_COMMIT && writable(memory.Protect & 0xffu); } - const auto written = subpagesOf(base, address, address + 1); + if (view.resident) { + resumeResidentWrites(base); + return true; + } invalidate(*view.page, written); protectSubpages(base, written, view.protection, "resume shared memory write", view.viewBytes); view.armed &= static_cast(~written); @@ -215,13 +224,17 @@ public: return true; } - void ForgetSection(HANDLE section) { + bool ForgetSection(HANDLE section) { std::lock_guard lock(mutex); const auto key = reinterpret_cast(section); - physical.erase(physical.lower_bound(std::make_pair(key, std::uint64_t{0})), physical.lower_bound(std::make_pair(key + 1, std::uint64_t{0}))); + const auto first = physical.lower_bound(std::make_pair(key, std::uint64_t{0})); + const auto last = physical.lower_bound(std::make_pair(key + 1, std::uint64_t{0})); + const bool blank = std::none_of(first, last, [](const auto& entry) { return entry.second->stored || !entry.second->page.expired(); }); + physical.erase(first, last); + return blank; } - bool BeginHostWrite(std::uintptr_t address, std::size_t bytes) { + bool BeginHostWrite(std::uintptr_t address, std::size_t bytes, bool store = true) { std::lock_guard lock(mutex); const auto first = views.lower_bound(address & ~(pageBytes - 1)); const auto end = address + bytes; @@ -230,10 +243,12 @@ public: } for (auto it = first; it != views.end() && it->first < end; ++it) { auto& view = it->second; - ++view.hostWrites; + if (store) ++view.hostWrites; + else ++view.hostImports; const auto written = subpagesOf(it->first, address, end); - invalidate(*view.page, written); - const auto opened = static_cast(view.armed & written); + if (store) invalidate(*view.page, written); + else if (view.resident) forgetClean(it->first, it->first + pageBytes); + const auto opened = static_cast((store || view.resident) ? view.armed & written : 0); if (opened == 0) continue; protectSubpages(it->first, opened, view.protection, "open shared memory to a host write", view.viewBytes); view.armed &= static_cast(~opened); @@ -241,12 +256,14 @@ public: return true; } - void EndHostWrite(std::uintptr_t address, std::size_t bytes) { + void EndHostWrite(std::uintptr_t address, std::size_t bytes, bool store = true) { std::lock_guard lock(mutex); const auto end = address + bytes; for (auto it = views.lower_bound(address & ~(pageBytes - 1)); it != views.end() && it->first < end; ++it) { - --it->second.hostWrites; - invalidate(*it->second.page, subpagesOf(it->first, address, end)); + if (store) --it->second.hostWrites; + else --it->second.hostImports; + if (store) invalidate(*it->second.page, subpagesOf(it->first, address, end)); + else if (it->second.resident) forgetClean(it->first, it->first + pageBytes); } } @@ -363,7 +380,7 @@ public: const auto available = capacity - *count; if (available == 0) return true; const auto run = std::prev(residentRuns.upper_bound(cursor)); - const auto stop = std::min({end, run->second.end, (cursor & ~std::uintptr_t{4095}) + available * 4096}); + const auto stop = std::min({end, run->second.end, (cursor & ~std::uintptr_t{4095}) + available * 4096, nextClean == cleanRanges.end() ? end : nextClean->first}); collectResident(run->first, run->second, cursor, stop, pages, count, clear, collection); if (*count == capacity) return true; cursor = stop; @@ -514,11 +531,24 @@ private: ~Section() { CloseHandle(handle); } }; struct Saved { - unsigned char* bytes; - explicit Saved(unsigned char* bytes) : bytes(bytes) {} + unsigned char* bytes = nullptr; + std::size_t length; + std::array committed{}; + explicit Saved(std::size_t length) : length(length) { + bytes = static_cast(VirtualAlloc(nullptr, length, MEM_RESERVE, PAGE_READWRITE)); + if (bytes == nullptr) fail("reserve unmapped direct memory"); + } Saved(const Saved&) = delete; Saved& operator=(const Saved&) = delete; ~Saved() { VirtualFree(bytes, 0, MEM_RELEASE); } + + void Commit(std::size_t offset) { + const auto page = offset / pageBytes; + const auto bit = std::uint64_t{1} << (page & 63); + if ((committed[page >> 6] & bit) != 0) return; + if (!VirtualAlloc(bytes + offset, pageBytes, MEM_COMMIT, PAGE_READWRITE)) fail("keep unmapped direct memory"); + committed[page >> 6] |= bit; + } }; struct Physical { std::weak_ptr page; @@ -538,11 +568,13 @@ private: bool resident = false; std::shared_ptr slot; std::size_t viewBytes = pageBytes; + std::uint32_t hostImports = 0; }; struct Run { std::uintptr_t end; std::array collected; std::uint32_t fresh; + bool writeObserved = false; }; struct Window { void* view = nullptr; @@ -556,6 +588,16 @@ private: void collectResident(std::uintptr_t start, Run& run, std::uintptr_t from, std::uintptr_t stop, void** pages, std::size_t* count, bool clear, Collection collection) { const auto first = from & ~std::uintptr_t{4095}; + const bool armed = clear && run.fresh == 0 && !run.writeObserved && collection != Collection::Fresh && run.end - start >= 0x40000 && armResident(first, stop); + if (run.fresh == 0 && collection != Collection::Fresh) { + ULONG_PTR reported = (stop - first + 4095) / 4096; + DWORD granularity = 0; + if (GetWriteWatch(clear ? WRITE_WATCH_FLAG_RESET : 0, reinterpret_cast(first), stop - first, pages + *count, &reported, &granularity) != 0) fail("collect resident guest writes"); + for (ULONG_PTR index = 0; index < reported; ++index) pages[*count + index] = reinterpret_cast(std::max(from, reinterpret_cast(pages[*count + index]))); + *count += reported; + if (armed) rememberClean(from, stop); + return; + } std::vector written; if (collection != Collection::Fresh) { std::vector found((stop - first + 4095) / 4096); @@ -591,6 +633,44 @@ private: if (collection == Collection::Fresh) flushReset(stop); } + void resumeResidentWrites(std::uintptr_t address) { + const auto run = std::prev(residentRuns.upper_bound(address)); + run->second.writeObserved = true; + forgetClean(run->first, run->second.end); + auto first = views.lower_bound(run->first); + while (first != views.end() && first->first < run->second.end) { + const auto begin = first->first; + auto end = begin; + const auto protection = first->second.protection; + auto next = first; + bool armed = false; + do { + armed = armed || next->second.armed != 0; + end = next->first + pageBytes; + ++next; + } while (next != views.end() && next->first == end && end < run->second.end && next->second.protection == protection); + if (armed) { + DWORD previous; + if (!VirtualProtect(reinterpret_cast(begin), end - begin, protection, &previous)) fail("resume resident guest writes"); + } + for (; first != next; ++first) first->second.armed = 0; + } + } + + bool armResident(std::uintptr_t from, std::uintptr_t stop) { + const auto first = views.lower_bound(from & ~(pageBytes - 1)); + if (first == views.end() || !writable(first->second.protection)) return false; + const auto protection = first->second.protection; + for (auto it = first; it != views.end() && it->first < stop; ++it) { + if (!it->second.resident || it->second.protection != protection || it->second.hostWrites != 0 || it->second.hostImports != 0 || it->second.page->pins != 0) return false; + } + DWORD previous; + const auto readOnly = protection == PAGE_EXECUTE_READWRITE ? PAGE_EXECUTE_READ : PAGE_READONLY; + if (!VirtualProtect(reinterpret_cast(from), stop - from, readOnly, &previous)) fail("arm resident memory write tracking"); + for (auto it = first; it != views.end() && it->first < stop; ++it) it->second.armed |= subpagesOf(it->first, from, stop); + return true; + } + Window open(HANDLE section, std::uint64_t offset, std::size_t bytes) { const auto lead = offset % granularity; Window window; @@ -682,12 +762,8 @@ private: const auto found = views.find(page); auto& view = found->second; if (!blank(page)) { - if (!saved) { - auto* bytes = static_cast(VirtualAlloc(nullptr, stop - start, MEM_RESERVE, PAGE_READWRITE)); - if (bytes == nullptr) fail("reserve unmapped direct memory"); - saved = std::make_shared(bytes); - } - if (!VirtualAlloc(saved->bytes + (page - start), pageBytes, MEM_COMMIT, PAGE_READWRITE)) fail("keep unmapped direct memory"); + if (!saved) saved = savedMemory(stop - start); + saved->Commit(page - start); std::memcpy(saved->bytes + (page - start), reinterpret_cast(page), pageBytes); view.slot->saved = saved; view.slot->savedOffset = page - start; @@ -699,6 +775,28 @@ private: residentRuns.erase(run); } + std::shared_ptr savedMemory(std::size_t bytes) { + for (const auto& saved : savedPool) { + if (saved.use_count() == 1 && saved->length == bytes) return saved; + } + constexpr std::size_t limit = 32u * 1024u * 1024u; + const auto reserved = (bytes + granularity - 1) / granularity * granularity; + for (auto it = savedPool.begin(); reserved <= limit && savedPoolBytes > limit - reserved && it != savedPool.end();) { + if (it->use_count() != 1) { + ++it; + continue; + } + savedPoolBytes -= ((*it)->length + granularity - 1) / granularity * granularity; + it = savedPool.erase(it); + } + auto saved = std::make_shared(bytes); + if (reserved <= limit && savedPoolBytes <= limit - reserved) { + savedPool.push_back(saved); + savedPoolBytes += reserved; + } + return saved; + } + static void forget(Ranges& ranges, std::uintptr_t start, std::uintptr_t end) { auto it = ranges.lower_bound(start); if (it != ranges.begin() && std::prev(it)->second > start) --it; @@ -815,6 +913,21 @@ private: forgetClean(address, end); breakRun(address); breakRun(end); + if (const auto run = residentRuns.find(address); run != residentRuns.end() && run->second.end == end) { + const auto memory = query(address); + if (reinterpret_cast(memory.AllocationBase) != address) throw std::runtime_error("cannot release part of a host allocation"); + if (memory.Type != MEM_PRIVATE) throw std::runtime_error("resident guest mapping is not private memory"); + auto allocationEnd = address; + do { + const auto part = query(allocationEnd); + if (part.AllocationBase != memory.AllocationBase) break; + allocationEnd = reinterpret_cast(part.BaseAddress) + part.RegionSize; + } while (allocationEnd < end); + if (allocationEnd != end || query(end).AllocationBase == memory.AllocationBase) throw std::runtime_error("guest release truncates a host allocation"); + evict(run); + if (!VirtualFree(reinterpret_cast(address), bytes, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) fail("release private guest memory"); + return; + } for (auto cursor = address; cursor < end;) { const auto memory = query(cursor); if (memory.State == MEM_RESERVE) { @@ -905,6 +1018,8 @@ private: } std::map, std::shared_ptr> physical; std::map residentRuns; + std::vector> savedPool; + std::size_t savedPoolBytes = 0; std::map splitAliases; std::size_t granularity = 0x10000; bool watchResident = false; diff --git a/core/libs/prx/libc/src/ApplicationHeap.cpp b/core/libs/prx/libc/src/ApplicationHeap.cpp index 538e038d40..84a8f46eb7 100644 --- a/core/libs/prx/libc/src/ApplicationHeap.cpp +++ b/core/libs/prx/libc/src/ApplicationHeap.cpp @@ -156,11 +156,12 @@ void ApplicationHeapInitialize_nid_no_patch(const void* processParameters) { const auto* replacement = read(libcParameters, 0x30); if (read(replacement, 0) != 0x78 || read(replacement, 8) != 2) throw std::runtime_error("application heap: unsupported allocator replacement table"); std::array api; - if (ApplicationGuestHeapApi_nid_no_patch(api.data())) { + std::memcpy(api.data(), static_cast(replacement) + 0x20, sizeof(api)); + const bool replaced = std::any_of(api.begin(), api.end(), [](const void* entry) { return entry != nullptr; }); + if (replaced && ApplicationGuestHeapApi_nid_no_patch(api.data())) { ApplicationHeapRegister_nid_no_patch(api.data()); return; } - std::memcpy(api.data(), static_cast(replacement) + 0x20, sizeof(api)); ApplicationHeapRegister_nid_no_patch(api.data()); const auto initialize = read(replacement, 0x10); if (initialize != nullptr) initialize(); diff --git a/core/libs/prx/libc/src/Backtrace.cpp b/core/libs/prx/libc/src/Backtrace.cpp index 29c7dae111..a07a476b4a 100644 --- a/core/libs/prx/libc/src/Backtrace.cpp +++ b/core/libs/prx/libc/src/Backtrace.cpp @@ -7,9 +7,35 @@ #include "prx/libc/include/General.hpp" -#ifdef __linux__ +#if defined(__linux__) #include #include +#elif defined(_WIN32) +#include +#include "prx/libc/include/exceptions/Unwind.hpp" +extern "C" { +_Unwind_Reason_Code APS5_VABI _Unwind_Backtrace_nid_postfix(_Unwind_Trace_Fn, void*); +_Unwind_Ptr APS5_VABI _Unwind_GetIP_nid_postfix(_Unwind_Context*); +std::string BacktraceModulePath_nid_no_patch(HMODULE handle, std::size_t initialCapacity) { + std::string module; + std::size_t capacity = initialCapacity == 0 ? 1 : initialCapacity; + while (true) { + module.resize(capacity); + SetLastError(ERROR_SUCCESS); + const DWORD length = GetModuleFileNameA(handle, module.data(), static_cast(module.size())); + if (length == 0) return {}; + if (length < capacity && GetLastError() != ERROR_INSUFFICIENT_BUFFER) { + module.resize(length); + return module; + } + if (capacity >= 32768) return {}; + capacity *= 2; + } +} +} +#endif + +#if defined(__linux__) || defined(_WIN32) namespace { @@ -31,15 +57,45 @@ static_assert(sizeof(FrameRecord) == NameOffset); [[gnu::noinline]] std::vector CaptureFrames(int limit) { void* addresses[MaxFrames]; +#if defined(__linux__) const int captured = backtrace(addresses, MaxFrames); +#elif defined(_WIN32) + struct TraceState { + void** addrs; + int max; + int count; + }; + static const auto traceCallback = [](_Unwind_Context* ctx, void* arg) APS5_VABI -> _Unwind_Reason_Code { + auto* s = static_cast(arg); + if (s->count < s->max) { + s->addrs[s->count++] = reinterpret_cast(_Unwind_GetIP_nid_postfix(ctx)); + } + return (s->count >= s->max) ? _URC_NORMAL_STOP : _URC_NO_REASON; + }; + TraceState state{addresses, MaxFrames, 0}; + _Unwind_Backtrace_nid_postfix(traceCallback, &state); + const int captured = state.count; +#endif std::vector frames; for (int index = 2; index < captured && (limit < 0 || static_cast(frames.size()) < limit); ++index) { +#if defined(__linux__) Dl_info info{}; if (dladdr(addresses[index], &info) == 0 || info.dli_fbase == nullptr || info.dli_fname == nullptr) continue; const auto offset = reinterpret_cast(addresses[index]) - reinterpret_cast(info.dli_fbase); if (offset > UINT32_MAX) continue; std::string module = info.dli_fname; - if (const auto slash = module.find_last_of('/'); slash != std::string::npos) module.erase(0, slash + 1); +#elif defined(_WIN32) + HMODULE handle = nullptr; + if (!GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + static_cast(addresses[index]), &handle) || handle == nullptr) { + continue; + } + const auto offset = reinterpret_cast(addresses[index]) - reinterpret_cast(handle); + if (offset > UINT32_MAX) continue; + std::string module = BacktraceModulePath_nid_no_patch(handle, MAX_PATH); + if (module.empty()) continue; +#endif + if (const auto slash = module.find_last_of("/\\"); slash != std::string::npos) module.erase(0, slash + 1); if (module.size() > sizeof(GuestSuffix) - 1 && module.compare(module.size() - (sizeof(GuestSuffix) - 1), std::string::npos, GuestSuffix) == 0) module.resize(module.size() - (sizeof(GuestSuffix) - 1)); frames.push_back({static_cast(offset), std::move(module)}); 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/BoundsChecking.cpp b/core/libs/prx/libc/src/BoundsChecking.cpp new file mode 100644 index 0000000000..a29df4a74f --- /dev/null +++ b/core/libs/prx/libc/src/BoundsChecking.cpp @@ -0,0 +1,298 @@ +#include +#include +#include +#include + +#include "prx/libc/include/FileStream.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/SonyCrt.hpp" + +extern "C" { +std::size_t APS5_VABI mbrtowc_nid_postfix(std::uint16_t* destination, const char* source, std::size_t count, void* state); +std::size_t APS5_VABI wcrtomb_nid_postfix(char* destination, std::uint16_t value, void* state); +int APS5_VABI wctomb_nid_postfix(char* destination, std::uint16_t value); +int APS5_VABI ___mb_cur_max_nid_postfix(); +int APS5_VABI getchar_nid_postfix(); +int APS5_VABI fputs_nid_postfix(const char* str, FileStream* stream); +int APS5_VABI fputc_nid_postfix(int value, FileStream* stream); +extern FileStream* __stderrp_nid_postfix; +[[noreturn]] void abort_nid_postfix(std::uint64_t, std::uint64_t, std::uint64_t, std::uint64_t, std::uint64_t, std::uint64_t); +} + +namespace { + +constexpr int guestEinval = 22; +constexpr int guestErange = 34; +constexpr int guestEilseq = 86; +constexpr int guestEof = -1; +constexpr std::size_t failed = static_cast(-1); +constexpr std::size_t rsizeMax = (std::size_t{1} << 61) - 1; +constexpr std::size_t guestStateSize = 128; + +using GuestContextCompare = int (APS5_VABI*)(const void*, const void*, void*); + +bool usableSize(std::size_t size) { + return size != 0 && size <= rsizeMax; +} + +bool convertFailed(std::size_t length) { + return length >= failed - 1; +} + +int violation(const char* message, int error) { + SonyInvokeConstraintHandler(message, nullptr, error); + return error; +} + +} // namespace + +extern "C" { + +void APS5_VABI abort_handler_s_nid_postfix(const char* message, void*, int) { + fputs_nid_postfix(message, __stderrp_nid_postfix); + fputc_nid_postfix('\n', __stderrp_nid_postfix); + abort_nid_postfix(0, 0, 0, 0, 0, 0); +} + +void* APS5_VABI bsearch_s_nid_postfix(const void* key, const void* base, std::size_t count, std::size_t size, GuestContextCompare compare, void* context) { + if (count > rsizeMax || size > rsizeMax) { + violation("bsearch_s: bad argument", guestEinval); + return nullptr; + } + if (count == 0) return nullptr; + if (!key || !base || !compare) { + violation("bsearch_s: bad argument", guestEinval); + return nullptr; + } + const auto* low = static_cast(base); + for (std::size_t remaining = count; remaining != 0; remaining >>= 1) { + const auto* probe = low + (remaining >> 1) * size; + const int order = compare(key, probe, context); + if (order == 0) return const_cast(probe); + if (order > 0) { + low = probe + size; + --remaining; + } + } + return nullptr; +} + +std::uint16_t* APS5_VABI wcstok_s_nid_postfix(std::uint16_t* str, std::size_t* remaining, const std::uint16_t* delimiters, std::uint16_t** context) { + if (!remaining || !context || !delimiters || *remaining > rsizeMax || (!str && !*context)) { + violation("wcstok_s: bad argument", guestEinval); + return nullptr; + } + if (!str) str = *context; + const auto isDelimiter = [delimiters](std::uint16_t value) { + for (const auto* delimiter = delimiters; *delimiter != 0; ++delimiter) + if (*delimiter == value) return true; + return false; + }; + const std::size_t available = *remaining; + std::size_t skipped = 0; + if (available == 0) { + violation("wcstok_s: missing token", guestEinval); + return nullptr; + } + while (str[skipped] != 0 && isDelimiter(str[skipped])) { + if (++skipped == available) { + violation("wcstok_s: missing token", guestEinval); + return nullptr; + } + } + std::uint16_t* const token = str + skipped; + const std::size_t left = available - skipped; + *remaining = left; + if (*token == 0) { + *context = token; + return nullptr; + } + std::size_t length = 0; + while (token[length] != 0 && !isDelimiter(token[length])) { + if (++length == left) { + violation("wcstok_s: missing delimiter", guestEinval); + return nullptr; + } + } + std::uint16_t* next = token + length; + *remaining = left - length; + if (*next != 0) { + *next++ = 0; + if (left != length) *remaining = left - length - 1; + } + *context = next; + return token; +} + +int APS5_VABI wctomb_s_nid_postfix(int* status, char* destination, std::size_t capacity, std::uint16_t wide) { + if (!status || (!destination && capacity) || (destination && capacity > rsizeMax)) + return violation("wctomb_s: bad argument", guestEinval); + if (destination && static_cast(___mb_cur_max_nid_postfix()) > capacity) { + char bytes[MB_LEN_MAX]; + const int length = wctomb_nid_postfix(bytes, wide); + if (length > 0 && static_cast(length) > capacity) return violation("wctomb_s: buffer too small", guestErange); + if (length > 0) std::memcpy(destination, bytes, static_cast(length)); + *status = length; + return length < 0 ? guestEilseq : 0; + } + const int length = wctomb_nid_postfix(destination, wide); + *status = length; + return destination && length < 0 ? guestEilseq : 0; +} + +int APS5_VABI wcrtomb_s_nid_postfix(std::size_t* result, char* destination, std::size_t capacity, std::uint16_t wide, void* state) { + if (!result || !state) { + if (destination && usableSize(capacity)) destination[0] = '\0'; + if (result) *result = failed; + return violation("wcrtomb_s: bad argument", guestEinval); + } + if ((!destination && capacity) || (destination && !usableSize(capacity))) { + *result = failed; + return violation("wcrtomb_s: bad argument", guestEinval); + } + char bytes[MB_LEN_MAX]; + std::size_t length = 0; + if (!destination) { + length = wcrtomb_nid_postfix(bytes, 0, state); + } else if (static_cast(___mb_cur_max_nid_postfix()) > capacity) { + length = wcrtomb_nid_postfix(bytes, wide, state); + if (length != failed && length > capacity) { + *result = failed; + destination[0] = '\0'; + return violation("wcrtomb_s: buffer too small", guestErange); + } + if (length != failed) std::memcpy(destination, bytes, length); + } else { + length = wcrtomb_nid_postfix(destination, wide, state); + } + *result = length; + return length == failed ? guestEilseq : 0; +} + +int APS5_VABI mbstowcs_s_nid_postfix(std::size_t* result, std::uint16_t* destination, std::size_t capacity, const char* source, std::size_t limit) { + if (!result || !source || (!destination && capacity) || (destination && (!usableSize(capacity) || limit > rsizeMax))) { + if (result) *result = failed; + if (destination && usableSize(capacity)) destination[0] = 0; + return violation("mbstowcs_s: bad argument", guestEinval); + } + alignas(8) unsigned char state[guestStateSize]{}; + if (!destination) { + std::size_t count = 0; + for (;; ++count) { + std::uint16_t wide = 0; + const std::size_t length = mbrtowc_nid_postfix(&wide, source, INT_MAX, state); + if (convertFailed(length)) { + *result = failed; + return guestEilseq; + } + if (length == 0 && wide == 0) break; + source += length; + } + *result = count; + return 0; + } + const std::size_t total = limit < capacity ? limit : capacity; + std::size_t index = 0; + while (index < total) { + const std::size_t length = mbrtowc_nid_postfix(destination + index, source, INT_MAX, state); + if (convertFailed(length)) { + *result = failed; + destination[index] = 0; + return guestEilseq; + } + if (length == 0 && destination[index] == 0) { + *result = index; + return 0; + } + source += length; + ++index; + } + if (index == capacity) { + *result = failed; + destination[0] = 0; + return violation("mbstowcs_s: buffer overflow", guestErange); + } + *result = index; + destination[index] = 0; + return 0; +} + +int APS5_VABI mbsrtowcs_s_nid_postfix(std::size_t* result, std::uint16_t* destination, std::size_t capacity, const char** source, std::size_t limit, void* state) { + if (!result || !source || !*source || !state || (!destination && capacity) || (destination && (!usableSize(capacity) || limit > rsizeMax))) { + if (destination && usableSize(capacity)) destination[0] = 0; + if (result) *result = failed; + return violation("mbsrtowcs_s: bad argument", guestEinval); + } + const char* cursor = *source; + if (!destination) { + std::size_t count = 0; + for (;; ++count) { + std::uint16_t wide = 0; + const std::size_t length = mbrtowc_nid_postfix(&wide, cursor, INT_MAX, state); + if (convertFailed(length)) { + *result = failed; + return guestEilseq; + } + if (length == 0 && wide == 0) break; + cursor += length; + } + *result = count; + return 0; + } + const std::size_t total = limit < capacity ? limit : capacity; + std::size_t index = 0; + while (index < total) { + const std::size_t length = mbrtowc_nid_postfix(destination + index, cursor, INT_MAX, state); + if (convertFailed(length)) { + *source = cursor; + destination[index] = 0; + *result = failed; + return guestEilseq; + } + if (length == 0 && destination[index] == 0) { + *source = nullptr; + *result = index; + return 0; + } + cursor += length; + ++index; + } + if (total != 0) *source = cursor; + if (index == capacity) { + destination[0] = 0; + *result = failed; + return violation("mbsrtowcs_s: buffer too small", guestErange); + } + destination[index] = 0; + *result = index; + return 0; +} + +char* APS5_VABI gets_s_nid_postfix(char* buffer, std::size_t size) { + if (size == 0) { + violation("gets_s: zero buffer size", guestEinval); + return nullptr; + } + if (!buffer || size > rsizeMax) { + violation("gets_s: bad argument", guestEinval); + return nullptr; + } + char* cursor = buffer; + int value = getchar_nid_postfix(); + for (std::size_t left = size - 1; left != 0 && value != guestEof && value != '\n'; --left) { + *cursor++ = static_cast(value); + value = getchar_nid_postfix(); + } + if (value == '\n' || (value == guestEof && cursor != buffer)) { + *cursor = '\0'; + return buffer; + } + if (value != guestEof) { + violation("gets_s: short buffer", guestErange); + while (value != guestEof && value != '\n') value = getchar_nid_postfix(); + } + buffer[0] = '\0'; + return nullptr; +} + +} diff --git a/core/libs/prx/libc/src/CxxAbiSupport.cpp b/core/libs/prx/libc/src/CxxAbiSupport.cpp index 2553a80f7c..1cbe9c38cd 100644 --- a/core/libs/prx/libc/src/CxxAbiSupport.cpp +++ b/core/libs/prx/libc/src/CxxAbiSupport.cpp @@ -18,6 +18,8 @@ #include #include #include +#include +#include #include #include "prx/libc/include/General.hpp" @@ -99,12 +101,28 @@ const std::error_category* _ZSt17iostream_categoryv_nid_postfix() { return &std: int APS5_VABI _ZSt13_Execute_onceRSt9once_flagPFiPvS1_PS1_ES1__nid_postfix( int* flag, int (APS5_VABI *callback)(void*, void*, void**), void* arg ) { - static std::recursive_mutex mutex; - std::lock_guard lock(mutex); + static std::mutex mutex; + static std::condition_variable changed; + static std::unordered_set running; + std::unique_lock lock(mutex); + changed.wait(lock, [&] { return *flag != 0 || !running.contains(flag); }); if (*flag != 0) return 1; - if (callback(nullptr, arg, nullptr) == 0) return 0; - *flag = 1; - return 1; + running.insert(flag); + lock.unlock(); + bool initialized; + try { + initialized = callback(nullptr, arg, nullptr) != 0; + } catch (...) { + lock.lock(); + running.erase(flag); + changed.notify_all(); + throw; + } + lock.lock(); + if (initialized) *flag = 1; + running.erase(flag); + changed.notify_all(); + return initialized ? 1 : 0; } } diff --git a/core/libs/prx/libc/src/Directory.cpp b/core/libs/prx/libc/src/Directory.cpp index f86c502cfe..425669cdcf 100644 --- a/core/libs/prx/libc/src/Directory.cpp +++ b/core/libs/prx/libc/src/Directory.cpp @@ -3,11 +3,18 @@ #include "prx/libc/include/FilesystemError.hpp" #include "prx/libc/include/AppArchive.hpp" #include +#include #include #include #include #include #include +#include +#ifdef _WIN32 +#include "prx/libc/include/WindowsDescriptor.hpp" +#include +#include +#endif namespace { int DirectoryError(int error) { @@ -27,19 +34,102 @@ int DirectoryError(int error) { } } #ifdef _WIN32 -using NativeDirectory = _WDIR; -NativeDirectory* OpenNative(const std::filesystem::path& path) { return ::_wopendir(path.c_str()); } -int CloseNative(NativeDirectory* native) { return ::_wclosedir(native); } -void RewindNative(NativeDirectory* native) { ::_wrewinddir(native); } -long TellNative(NativeDirectory* native) { return ::_wtelldir(native); } -void SeekNative(NativeDirectory* native, long location) { ::_wseekdir(native, location); } +struct NativeDirectory { + HANDLE handle = INVALID_HANDLE_VALUE; + std::uint32_t seed = 0; + bool identifiers = true; + bool restart = true; + bool buffered = false; + std::size_t next = 0; + long position = 0; + alignas(8) std::byte buffer[65536]; + ~NativeDirectory() { if (handle != INVALID_HANDLE_VALUE) CloseHandle(handle); } +}; +int NativeOpenError(DWORD error) { + if (error == ERROR_CANT_RESOLVE_FILENAME) return ELOOP; + if (error == ERROR_DIRECTORY) return ENOTDIR; + return WindowsOpenError_nid_no_patch(error); +} +NativeDirectory* OpenNative(const std::filesystem::path& path) { + const DWORD attributes = GetFileAttributesW(path.c_str()); + if (attributes == INVALID_FILE_ATTRIBUTES) { errno = NativeOpenError(GetLastError()); return nullptr; } + if ((attributes & FILE_ATTRIBUTE_DIRECTORY) == 0) { errno = ENOTDIR; return nullptr; } + auto native = std::make_unique_for_overwrite(); + native->handle = CreateFileW(path.c_str(), FILE_LIST_DIRECTORY | SYNCHRONIZE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (native->handle == INVALID_HANDLE_VALUE) { errno = NativeOpenError(GetLastError()); return nullptr; } + BY_HANDLE_FILE_INFORMATION identity{}; + if (GetFileInformationByHandle(native->handle, &identity)) { + if ((identity.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0) { errno = ENOTDIR; return nullptr; } + native->seed = WindowsFileNumber_nid_no_patch(identity); + } + return native.release(); +} +int CloseNative(NativeDirectory* native) { delete native; return 0; } +void RewindNative(NativeDirectory* native) { + native->restart = true; + native->buffered = false; + native->position = 0; +} +bool QueryNative(NativeDirectory* native) { + const auto query = [native](FILE_INFO_BY_HANDLE_CLASS restartClass, FILE_INFO_BY_HANDLE_CLASS nextClass) { + return GetFileInformationByHandleEx(native->handle, native->restart ? restartClass : nextClass, native->buffer, sizeof(native->buffer)) != 0; + }; + bool filled = native->identifiers ? query(FileIdBothDirectoryRestartInfo, FileIdBothDirectoryInfo) : query(FileFullDirectoryRestartInfo, FileFullDirectoryInfo); + DWORD error = filled ? ERROR_SUCCESS : GetLastError(); + if (!filled && native->restart && native->identifiers && (error == ERROR_INVALID_PARAMETER || error == ERROR_INVALID_FUNCTION || + error == ERROR_INVALID_LEVEL || error == ERROR_NOT_SUPPORTED)) { + native->identifiers = false; + filled = query(FileFullDirectoryRestartInfo, FileFullDirectoryInfo); + error = filled ? ERROR_SUCCESS : GetLastError(); + } + if (!filled) { + if (error != ERROR_NO_MORE_FILES && error != ERROR_FILE_NOT_FOUND) errno = EIO; + return false; + } + native->restart = false; + native->buffered = true; + native->next = 0; + return true; +} +const std::byte* NextRecord(NativeDirectory* native) { + if (!native->buffered && !QueryNative(native)) return nullptr; + const std::byte* record = native->buffer + native->next; + const DWORD offset = native->identifiers ? reinterpret_cast(record)->NextEntryOffset + : reinterpret_cast(record)->NextEntryOffset; + if (offset == 0) native->buffered = false; + native->next += offset; + ++native->position; + return record; +} +std::uint32_t StableFileNumber(std::uint32_t seed, std::wstring_view name) { + std::uint32_t hash = 2166136261u ^ seed; + for (const wchar_t character : name) hash = (hash ^ static_cast(character)) * 16777619u; + return hash == 0 ? 1 : hash; +} +long TellNative(NativeDirectory* native) { return native->position; } +void SeekNative(NativeDirectory* native, long location) { + if (location < 0) return; + RewindNative(native); + while (native->position < location && NextRecord(native)) {} +} bool ReadNative(NativeDirectory* native, std::filesystem::path& component, std::string& name, std::uint64_t& inode) { - auto* entry = ::_wreaddir(native); - if (!entry) return false; - component = entry->d_name; + const std::byte* record = NextRecord(native); + if (!record) return false; + std::wstring_view fileName; + std::uint64_t fileId = 0; + if (native->identifiers) { + const auto* information = reinterpret_cast(record); + fileName = std::wstring_view(information->FileName, information->FileNameLength / sizeof(WCHAR)); + fileId = static_cast(information->FileId.QuadPart); + } else { + const auto* information = reinterpret_cast(record); + fileName = std::wstring_view(information->FileName, information->FileNameLength / sizeof(WCHAR)); + } + component = fileName; const auto utf8 = component.u8string(); name.assign(utf8.begin(), utf8.end()); - inode = static_cast(entry->d_ino); + inode = fileId != 0 ? WindowsFileNumber_nid_no_patch(fileId) : StableFileNumber(native->seed, fileName); return true; } #else @@ -86,16 +176,17 @@ GuestDirectoryEntry* ReadEntry(Directory& directory) { std::filesystem::path component; std::string name; std::uint64_t inode = 0; - if (!ReadNative(directory.native, component, name, inode)) { - errno = errno ? DirectoryError(errno) : savedError; - return nullptr; - } + try { + if (!ReadNative(directory.native, component, name, inode)) { + errno = errno ? DirectoryError(errno) : savedError; + return nullptr; + } + } catch (const std::bad_alloc&) { errno = 12; return nullptr; } const auto length = name.size(); if (length > 255) { errno = 63; return nullptr; } try { directory.entry = {}; - // MinGW reports no inode identity (zero); do not fabricate one. - directory.entry.fileNumber = inode <= UINT32_MAX ? static_cast(inode) : 0; + directory.entry.fileNumber = static_cast(inode); directory.entry.recordLength = static_cast(8 + ((length + 1 + 3) & ~3)); directory.entry.nameLength = static_cast(length); directory.entry.type = DirectoryType(directory.path / component); @@ -116,7 +207,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 +273,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/Environment.cpp b/core/libs/prx/libc/src/Environment.cpp index cbb171729b..f77b8727f9 100644 --- a/core/libs/prx/libc/src/Environment.cpp +++ b/core/libs/prx/libc/src/Environment.cpp @@ -7,6 +7,7 @@ #include #include #include +#include "prx/libc/include/General.hpp" #include "prx/libc/include/general/VabiMacros.hpp" namespace { @@ -32,6 +33,17 @@ int EnvironmentError(int error) { errno = error; return -1; } } extern "C" { +bool CopyGuestEnvironment_nid_no_patch(const char* name, std::string& value) { + if (!ValidEnvironmentName(name)) return false; + std::lock_guard lock(environmentMutex); + for (auto& entry : Environment()) + if (entry.Matches(name)) { + value = entry.Data() + std::strlen(name) + 1; + return true; + } + return false; +} + char* APS5_VABI getenv_nid_postfix(const char* name) { if (!ValidEnvironmentName(name)) { errno = 22; return nullptr; } try { diff --git a/core/libs/prx/libc/src/Errors.cpp b/core/libs/prx/libc/src/Errors.cpp index 56a77382bb..4e2d3f08bb 100644 --- a/core/libs/prx/libc/src/Errors.cpp +++ b/core/libs/prx/libc/src/Errors.cpp @@ -1,3 +1,4 @@ +#include "prx/libc/include/FileStream.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include #include @@ -76,8 +77,9 @@ void APS5_VABI perror_nid_postfix(const char* prefix) { const int saved = *__error_nid_postfix(); char message[128]; strerror_r_nid_postfix(saved, message, sizeof(message)); - if (prefix && *prefix) std::fprintf(stderr, "%s: %s\n", prefix, message); - else std::fprintf(stderr, "%s\n", message); + auto* stream = GetNativeStream(&_Stderr_nid_postfix); + if (prefix && *prefix) std::fprintf(stream, "%s: %s\n", prefix, message); + else std::fprintf(stream, "%s\n", message); *__error_nid_postfix() = saved; } } diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index 3ad3615c15..410fa6f309 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. @@ -317,7 +323,7 @@ FileStream* APS5_VABI freopen_nid_postfix(const char* filename, const char* mode return nullptr; } } - if (stream->Reopen(path, parsed->native.c_str())) { + if (stream->Reopen(path, parsed->native.c_str(), OpenNative)) { #ifdef _WIN32 if (parsed->append && !AppendOnlyDescriptor(_fileno(stream->GetHandle()))) { const int error = errno; @@ -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()); @@ -434,6 +442,7 @@ size_t APS5_VABI fread_nid_postfix(void* buffer, size_t size, size_t count, File stream->SyncStatus(); return count; } + if (!stream->SwitchToReading()) return 0; const auto result = std::fread(buffer, size, count, handle); stream->SyncStatus(); return result; 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/FloatingPoint.cpp b/core/libs/prx/libc/src/FloatingPoint.cpp new file mode 100644 index 0000000000..e3717ef61b --- /dev/null +++ b/core/libs/prx/libc/src/FloatingPoint.cpp @@ -0,0 +1,133 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +namespace { + +constexpr unsigned Exceptions = 0x3f; +constexpr unsigned Rounding = 0xc00; + +void ReplaceFlags(unsigned mask, unsigned flags) { + std::array environment; + asm volatile("fnstenv %0" : "=m"(environment)); + environment[1] = (environment[1] & ~mask) | (flags & mask); + asm volatile("fldenv %0" : : "m"(environment)); + _mm_setcsr((_mm_getcsr() & ~mask) | (flags & mask)); +} + +int ChangeTraps(int exceptions, bool enable) { + const auto mask = static_cast(exceptions) & Exceptions; + std::uint16_t control; + asm volatile("fnstcw %0" : "=m"(control)); + const unsigned csr = _mm_getcsr(); + const auto changed = static_cast(enable ? (control & ~mask) : (control | mask)); + asm volatile("fldcw %0" : : "m"(changed)); + _mm_setcsr(enable ? (csr & ~(mask << 7)) : (csr | (mask << 7))); + return static_cast(~((csr >> 7) | control) | ~Exceptions); +} + +} + +extern "C" { + +int APS5_VABI fegetround_nid_postfix() { + std::uint16_t control; + asm volatile("fnstcw %0" : "=m"(control)); + return control & Rounding; +} + +int APS5_VABI fesetround_nid_postfix(int rounding) { + const auto mode = static_cast(rounding); + if ((mode & ~Rounding) != 0) return -1; + std::uint16_t control; + asm volatile("fnstcw %0" : "=m"(control)); + control = static_cast((control & ~Rounding) | mode); + asm volatile("fldcw %0" : : "m"(control)); + _mm_setcsr((_mm_getcsr() & ~(Rounding << 3)) | (mode << 3)); + return 0; +} + +int APS5_VABI fetestexcept_nid_postfix(int exceptions) { + std::uint16_t status; + asm volatile("fnstsw %0" : "=am"(status)); + return (status | _mm_getcsr()) & static_cast(exceptions) & Exceptions; +} + +int APS5_VABI fegetexceptflag_nid_postfix(std::uint16_t* flags, int exceptions) { + if (flags == nullptr) throw std::invalid_argument("fegetexceptflag: null flags"); + *flags = static_cast(fetestexcept_nid_postfix(exceptions)); + return 0; +} + +int* APS5_VABI __error_nid_postfix(); + +void APS5_VABI _Feraise_nid_postfix(int exceptions) { + constexpr unsigned Invalid = 0x01, Ranges = 0x1c, Overflows = 0x18, Inexact = 0x20; + constexpr int GuestEdom = 33, GuestErange = 34; + auto raised = static_cast(exceptions); + if ((raised & Overflows) != 0) raised |= Inexact; + *__error_nid_postfix() = GuestEdom; + std::array environment; + asm volatile("fnstenv %0" : "=m"(environment)); + environment[1] = (environment[1] & ~raised) | (raised & 0xffff); + asm volatile("fldenv %0" : : "m"(environment)); + _mm_setcsr((_mm_getcsr() & ~raised) | (raised & 0xffff)); + asm volatile("fwait"); + if ((raised & Invalid) != 0) *__error_nid_postfix() = GuestEdom; + else if ((raised & Ranges) != 0) *__error_nid_postfix() = GuestErange; +} + +int APS5_VABI fesetexceptflag_nid_postfix(const std::uint16_t* flags, int exceptions) { + if (flags == nullptr) throw std::invalid_argument("fesetexceptflag: null flags"); + ReplaceFlags(static_cast(exceptions) & Exceptions, *flags); + return 0; +} + +int APS5_VABI feclearexcept_nid_postfix(int exceptions) { + const auto mask = static_cast(exceptions) & Exceptions; + if (mask == Exceptions) { + asm volatile("fnclex"); + _mm_setcsr(_mm_getcsr() & ~Exceptions); + } else if (mask != 0) { + ReplaceFlags(mask, 0); + } + return 0; +} + + +int APS5_VABI feenableexcept_nid_postfix(int exceptions) { + return ChangeTraps(exceptions, true); +} + +int APS5_VABI fedisableexcept_nid_postfix(int exceptions) { + return ChangeTraps(exceptions, false); +} + +int APS5_VABI __feenableexcept_nid_postfix(int exceptions) { + return ChangeTraps(exceptions, true); +} + +int APS5_VABI __fedisableexcept_nid_postfix(int exceptions) { + return ChangeTraps(exceptions, false); +} + +int APS5_VABI fegettrapenable_nid_postfix() { + std::uint16_t control; + asm volatile("fnstcw %0" : "=m"(control)); + return ~control & Exceptions; +} + +int APS5_VABI fesettrapenable_nid_postfix(int exceptions) { + const auto mask = static_cast(exceptions); + if (mask > Exceptions) return 1; + std::uint16_t control; + asm volatile("fnstcw %0" : "=m"(control)); + control = static_cast(control & ~mask); + asm volatile("fldcw %0" : : "m"(control)); + _mm_setcsr(_mm_getcsr() & ~(static_cast(mask) << 7)); + return 0; +} + +} diff --git a/core/libs/prx/libc/src/Formatting.cpp b/core/libs/prx/libc/src/Formatting.cpp index 0d883aa904..07fc5468a8 100644 --- a/core/libs/prx/libc/src/Formatting.cpp +++ b/core/libs/prx/libc/src/Formatting.cpp @@ -9,6 +9,8 @@ #include #include #include +#include +#include #include "prx/libc/include/General.hpp" #include "SceTypes.hpp" @@ -16,6 +18,7 @@ #include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/WindowsFormatting.hpp" +#include "prx/libc/include/ScanWideField.hpp" #ifdef _WIN32 #include "prx/libc/include/WindowsScanning.hpp" @@ -44,14 +47,14 @@ bool RequiresGuestFormatting(const char* format) { return false; } -void RejectGuestWideScan(const char* format) { - if (format == nullptr) return; +bool HasGuestWideScan(const char* format) { + if (format == nullptr) return false; while ((format = std::strchr(format, '%')) != nullptr) { ++format; if (*format == '%') { ++format; continue; } while (*format && std::strchr("-+ #0'0123456789.*$", *format)) ++format; if (*format == 'S' || *format == 'C' || (*format == 'l' && format[1] != '\0' && std::strchr("sc[", format[1]))) - NotImplemented_nid_no_patch("scanf 16-bit wide conversion on Linux"); + return true; if (*format == '[') { ++format; if (*format == '^') ++format; @@ -60,7 +63,24 @@ void RejectGuestWideScan(const char* format) { } if (*format) ++format; } + return false; } + +bool ScansWideInProcess(const char* format) { + if (!HasGuestWideScan(format)) return false; + LibcDetail::RequireNoPositionalWideScan(format); + return true; +} + +void RejectGuestWideScan(const char* format) { + if (HasGuestWideScan(format)) NotImplemented_nid_no_patch("fscanf 16-bit wide conversion on Linux"); +} + +bool IsWideScan(const std::string& length, char conversion) { + return conversion == 'S' || conversion == 'C' || (length == "l" && (conversion == 's' || conversion == 'c' || conversion == '[')); +} + + #endif std::string HostLength(const std::string& length, char conversion) { @@ -155,6 +175,58 @@ int ScanGuest(const char* buffer, const char* format, bool secure, NextPointer n } continue; } +#ifndef _WIN32 + if (IsWideScan(length, conversion)) { + const char kind = conversion == 'S' ? 's' : conversion == 'C' ? 'c' : conversion; + const char* field = input; + if (kind == 's') { + while (std::isspace(static_cast(*field))) ++field; + } + if (*field == '\0') return finish(converted ? assigned : EOF); + auto* target = suppress ? nullptr : static_cast(nextPointer()); + unsigned long limit = ULONG_MAX; + if (secure && !suppress) { + const auto capacity = static_cast(nextCapacity()); + limit = kind == 'c' ? capacity : (capacity == 0 ? 0 : capacity - 1); + if (limit == 0 || (kind == 'c' && (width == 0 ? 1 : width) > limit)) { + if (capacity != 0) target[0] = u'\0'; + return finish(assigned); + } + } + const unsigned long wanted = width != 0 ? width : (kind == 'c' ? 1 : ULONG_MAX); + const unsigned long taken = std::min(wanted, limit); + std::u16string text; + const auto accepts = [&](char32_t point) { + if (kind == 's') return point >= 0x80 || !std::isspace(static_cast(point)); + if (kind == '[') return LibcDetail::InWideScanset(specifier, point); + return true; + }; + while (text.size() < taken && *field != '\0') { + char32_t point = 0; + const std::size_t size = LibcDetail::ReadWideScanCharacter(field, &point); + if (!accepts(point)) break; + text.push_back(static_cast(point)); + field += size; + } + if (text.empty()) return finish(assigned); + if (target != nullptr && kind != 'c' && wanted > limit && text.size() == limit && *field != '\0') { + char32_t point = 0; + LibcDetail::ReadWideScanCharacter(field, &point); + if (accepts(point)) { + target[0] = u'\0'; + return finish(assigned); + } + } + if (target != nullptr) { + std::memcpy(target, text.data(), text.size() * sizeof(char16_t)); + if (kind != 'c') target[text.size()] = u'\0'; + ++assigned; + } + input = field; + converted = true; + continue; + } +#endif if (conversion != 'c' && conversion != '[') { const char* probe = input; while (std::isspace(static_cast(*probe))) ++probe; @@ -292,6 +364,7 @@ int APS5_VABI fprintf_nid_postfix(FileStream* stream, const char* format, ...) { int APS5_VABI fscanf_nid_postfix(FileStream* stream, const char* format, ...) { if (stream && !stream->IsOpen()) { errno = 9; return -1; } auto* native = GetNativeStream(stream); + if (!stream->SwitchToReading()) return EOF; #ifdef _WIN32 __builtin_sysv_va_list args; __builtin_sysv_va_start(args, format); @@ -350,7 +423,7 @@ int APS5_VABI printf_nid_postfix(const char* format, ...) { if (!_Stdout_nid_postfix.IsOpen()) { errno = 9; return -1; } std::va_list args; va_start(args, format); - const int result = RequiresGuestFormatting(format) ? LibcDetail::PrintWindows(format, args) : std::vprintf(format, args); + const int result = RequiresGuestFormatting(format) ? LibcDetail::PrintWindows(format, args) : std::vfprintf(GuestStandardOutputFile_nid_no_patch(), format, args); va_end(args); return result; } @@ -359,7 +432,7 @@ int APS5_VABI libc_printf_nid_postfix(const char* format, ...) { if (!_Stdout_nid_postfix.IsOpen()) { errno = 9; return -1; } std::va_list args; va_start(args, format); - const int result = RequiresGuestFormatting(format) ? LibcDetail::PrintWindows(format, args) : std::vprintf(format, args); + const int result = RequiresGuestFormatting(format) ? LibcDetail::PrintWindows(format, args) : std::vfprintf(GuestStandardOutputFile_nid_no_patch(), format, args); va_end(args); return result; } @@ -386,7 +459,10 @@ int APS5_VABI vsscanf_nid_postfix(const char* input, const char* format, VaList* #ifdef _WIN32 return LibcDetail::ScanWindows(input, format, args); #else - RejectGuestWideScan(format); + if (ScansWideInProcess(format)) { + LibcDetail::FormatArguments arguments(args); + return ScanGuest(input, format, false, [&] { return arguments.Next(); }, [] { return 0u; }); + } std::va_list copy; va_copy(copy, *reinterpret_cast(args)); const int result = HostVsscanf(input, format, copy); @@ -405,10 +481,10 @@ int APS5_VABI sscanf_nid_postfix(const char* input, const char* format, ...) { } #else int APS5_VABI sscanf_nid_postfix(const char* input, const char* format, ...) { - RejectGuestWideScan(format); std::va_list args; va_start(args, format); - const int result = HostVsscanf(input, format, args); + const int result = ScansWideInProcess(format) ? ScanGuest(input, format, false, [&] { return va_arg(args, void*); }, [] { return 0u; }) + : HostVsscanf(input, format, args); va_end(args); return result; } @@ -427,7 +503,6 @@ int APS5_VABI sscanf_s_nid_postfix(const char* buffer, const char* format, ...) #else int APS5_VABI sscanf_s_nid_postfix(const char* buffer, const char* format, ...) { - RejectGuestWideScan(format); std::va_list args; va_start(args, format); const int result = ScanGuest(buffer, format, true, [&] { return va_arg(args, void*); }, [&] { return va_arg(args, unsigned int); }); @@ -445,7 +520,7 @@ int APS5_VABI vprintf_nid_postfix(const char* str, VaList* c) { if (RequiresGuestFormatting(str)) return LibcDetail::PrintWindows(str, c); std::va_list copy; va_copy(copy, *reinterpret_cast(c)); - const int result = std::vprintf(str, copy); + const int result = std::vfprintf(GuestStandardOutputFile_nid_no_patch(), str, copy); va_end(copy); return result; #endif @@ -520,7 +595,6 @@ int APS5_VABI printf_s_nid_postfix(const char* format, ...) { #else int APS5_VABI vsscanf_s_nid_postfix(const char* buffer, const char* format, VaList* args) { - RejectGuestWideScan(format); LibcDetail::FormatArguments arguments(args); return ScanGuest(buffer, format, true, [&] { return arguments.Next(); }, [&] { return arguments.Next(); }); } @@ -553,7 +627,7 @@ int APS5_VABI snprintf_s_nid_postfix(char* buffer, size_t size, const char* form int APS5_VABI printf_s_nid_postfix(const char* format, ...) { std::va_list args; va_start(args, format); - const int result = RequiresGuestFormatting(format) ? LibcDetail::PrintWindows(format, args) : std::vprintf(format, args); + const int result = RequiresGuestFormatting(format) ? LibcDetail::PrintWindows(format, args) : std::vfprintf(GuestStandardOutputFile_nid_no_patch(), format, args); va_end(args); return result; } @@ -562,7 +636,48 @@ int APS5_VABI printf_s_nid_postfix(const char* format, ...) { int APS5_VABI puts_nid_postfix(const char* s) { if (!_Stdout_nid_postfix.IsOpen()) { errno = 9; return -1; } - return std::puts(s); + auto* native = GetNativeStream(&_Stdout_nid_postfix); + if (std::fputs(s, native) < 0 || std::fputc('\n', native) == EOF) return EOF; + return 0; +} + +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..a2b3d0fe7d 100644 --- a/core/libs/prx/libc/src/FormattingWide.cpp +++ b/core/libs/prx/libc/src/FormattingWide.cpp @@ -167,6 +167,8 @@ std::u16string FormatWide(const char16_t* format, VaList* source, bool secure = if (length == "h") value = static_cast(value); if (length == "hh") value = static_cast(value); } else value = args.Next(); + if (conversion == u'o' && value == 0 && precision == 0 && spec.find('#') != std::string::npos) + spec = spec.substr(0, spec.find('.')) + ".1"; AppendNumber(out, spec + "ll" + static_cast(conversion), value); } else if (In(conversion, "aAeEfFgG")) { if (length == "L") AppendNumber(out, spec + "L" + static_cast(conversion), args.Next()); @@ -325,7 +327,7 @@ int APS5_VABI wprintf_nid_postfix(const char16_t* format, ...) { try { const std::u16string text = FormatWide(format, reinterpret_cast(args)); const std::string bytes = ToUtf8(text); - if (std::fwrite(bytes.data(), 1, bytes.size(), stdout) == bytes.size()) result = static_cast(text.size()); + if (std::fwrite(bytes.data(), 1, bytes.size(), GetNativeStream(__stdoutp_nid_postfix)) == bytes.size()) result = static_cast(text.size()); } catch (const std::exception&) { errno = 22; } @@ -333,6 +335,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 97fc58606b..26c1935c5a 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" @@ -83,14 +84,73 @@ struct NameMatch { std::string name; std::optional path; bool ambiguous = false; + bool stem = false; }; +std::string_view NameStem(std::string_view name) { + const auto dot = name.rfind('.'); + return dot == std::string_view::npos || dot == 0 ? name : name.substr(0, dot); +} + +bool SameRelinkedStem(std::string_view filename, std::string_view name) { + constexpr std::string_view suffix = ".guest.prx"; + if (filename.size() <= suffix.size() || !SameName(filename.substr(filename.size() - suffix.size()), suffix)) return false; + return SameName(NameStem(filename.substr(0, filename.size() - suffix.size())), NameStem(name)); +} + +#ifdef _WIN32 +bool CaseInsensitiveDirectoryWindows(const std::filesystem::path& directory) { + const auto handle = CreateFileW(directory.c_str(), FILE_READ_ATTRIBUTES, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (handle == INVALID_HANDLE_VALUE) return false; + const std::unique_ptr owner(handle, &CloseHandle); + struct { ULONG flags; } information{}; + constexpr auto caseSensitiveInfo = static_cast(23); + return GetFileInformationByHandleEx(handle, caseSensitiveInfo, &information, sizeof(information)) && + (information.flags & FILE_CS_FLAG_CASE_SENSITIVE_DIR) == 0; +} + +std::error_code MatchNamesWindows(const std::filesystem::path& directory, std::span names) { + for (auto& name : names) { + const auto pattern = directory / name.name; + WIN32_FIND_DATAW data{}; + const auto handle = FindFirstFileExW(pattern.c_str(), FindExInfoBasic, &data, FindExSearchNameMatch, nullptr, 0); + if (handle == INVALID_HANDLE_VALUE) { + const auto error = GetLastError(); + if (error == ERROR_FILE_NOT_FOUND || error == ERROR_NO_MORE_FILES) continue; + return {static_cast(error), std::system_category()}; + } + const std::unique_ptr owner(handle, &FindClose); + do { + const auto filename = std::filesystem::path(data.cFileName); + if (!SameName(filename.string(), name.name)) continue; + name.ambiguous = name.ambiguous || name.path.has_value(); + name.path = directory / filename; + } while (FindNextFileW(handle, &data)); + const auto error = GetLastError(); + if (error != ERROR_NO_MORE_FILES) return {static_cast(error), std::system_category()}; + } + return {}; +} +#endif + std::error_code MatchNames(const std::filesystem::path& directory, std::span names) { +#ifdef _WIN32 + bool stem = false; + for (const auto& name : names) stem = stem || name.stem; + if (!stem && CaseInsensitiveDirectoryWindows(directory)) return MatchNamesWindows(directory, names); +#endif std::error_code error; std::filesystem::directory_iterator entry(directory, error), end; for (; !error && entry != end; entry.increment(error)) { const auto filename = entry->path().filename().string(); for (auto& name : names) { + if (name.stem) { + std::error_code status; + if (!SameRelinkedStem(filename, name.name) || !entry->is_regular_file(status) || status) continue; + if (!name.path || filename.size() < name.path->filename().string().size()) name.path = entry->path(); + continue; + } if (!SameName(filename, name.name)) continue; name.ambiguous = name.ambiguous || name.path.has_value(); name.path = entry->path(); @@ -141,8 +201,26 @@ std::filesystem::path Child(std::filesystem::path parent, const std::string& nam std::filesystem::path ResolveHostPath(std::filesystem::path root, const std::vector& relative, bool module = false) { const auto direct = Child(root, JoinNames(relative)); std::error_code error; + auto first = relative.begin(); +#ifdef _WIN32 + const auto attributes = GetFileAttributesW(direct.c_str()); + if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0) return direct; + const auto failure = GetLastError(); + const bool missing = attributes == INVALID_FILE_ATTRIBUTES && (failure == ERROR_FILE_NOT_FOUND || failure == ERROR_PATH_NOT_FOUND); + if (!missing && std::filesystem::exists(direct, error)) return direct; + for (auto last = relative.end(); last != relative.begin();) { + const auto parent = Child(root, JoinNames(std::vector(relative.begin(), std::prev(last)))); + const auto parentAttributes = GetFileAttributesW(parent.c_str()); + const bool present = parentAttributes != INVALID_FILE_ATTRIBUTES && (parentAttributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0; + if (!present && !std::filesystem::exists(parent, error)) { --last; continue; } + root = parent; + first = std::prev(last); + break; + } +#else if (std::filesystem::exists(direct, error)) return direct; - for (auto part = relative.begin(); part != relative.end(); ++part) { +#endif + for (auto part = first; part != relative.end(); ++part) { const auto candidate = Child(root, *part); const auto status = std::filesystem::symlink_status(candidate, error); if (std::filesystem::exists(status) || (error && error != std::errc::no_such_file_or_directory)) { @@ -246,6 +324,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. @@ -254,10 +346,12 @@ std::filesystem::path Resolve(WorkingDirectory& state, const char* path, bool mo if (text.empty() || text.front() != '/') text = state.current.lexically_relative(state.root).generic_string() + "/" + text; std::vector names; bool trailing = true; + bool endsWithDot = false; const auto parts = SplitNames(text); for (std::size_t i = 0; i < parts.size(); ++i) { const auto& name = parts[i]; if (name.empty() && i + 1 != parts.size()) continue; + if (!name.empty()) endsWithDot = name == "."; if (name == ".." && !names.empty()) { if (!(archived && AppArchiveHasDirectory_nid_no_patch(HostPath(state, names)))) RequireHostDirectory(state, WithTrailingSeparator(names, trailing)); names.pop_back(); @@ -265,6 +359,7 @@ std::filesystem::path Resolve(WorkingDirectory& state, const char* path, bool mo trailing = name.empty() || name == "." || name == ".."; if (!trailing) names.push_back(name); } + if (endsWithDot && !names.empty() && !(archived && AppArchiveHasDirectory_nid_no_patch(HostPath(state, names)))) RequireHostDirectory(state, names); return HostPath(state, WithTrailingSeparator(std::move(names), trailing), module); } #ifdef _WIN32 @@ -407,11 +502,63 @@ extern "C" int HostStandardError_nid_no_patch() { return g_hostStandardError.load(); } +namespace { +std::mutex g_hostStandardOutputMutex; +std::atomic g_hostStandardOutput{-1}; +} + +extern "C" int KeepHostStandardOutput_nid_no_patch() { + const std::lock_guard lock(g_hostStandardOutputMutex); + if (const int kept = g_hostStandardOutput.load(); kept >= 0) return kept; + const int kept = ::fcntl(1, F_DUPFD_CLOEXEC, 3); + if (kept < 0) { + if (errno == EBADF) return -1; + throw std::system_error(errno, std::generic_category(), "Keeping the host standard output"); + } + std::FILE* const stream = ::fdopen(kept, "w"); + if (stream == nullptr) { + const int error = errno; + ::close(kept); + throw std::system_error(error, std::generic_category(), "Keeping the host standard output"); + } + std::setvbuf(stream, nullptr, _IONBF, 0); + std::fflush(stdout); + stdout = stream; + g_hostStandardOutput.store(kept); + return kept; +} + extern "C" int DuplicateHostDescriptor_nid_no_patch(int descriptor) { return ::fcntl(descriptor, F_DUPFD_CLOEXEC, 3); } #endif +#ifdef _WIN32 +namespace { +std::atomic g_hostStandardOutputFile{nullptr}; +std::atomic g_hostStandardErrorFile{nullptr}; +} + +extern "C" bool AdoptHostStandardStream_nid_no_patch(int standard, int saved) { + std::fflush(__acrt_iob_func(standard)); + std::FILE* stream = ::_fdopen(saved, "w"); + if (stream == nullptr) return false; + std::setvbuf(stream, nullptr, _IONBF, 0); + (standard == 1 ? g_hostStandardOutputFile : g_hostStandardErrorFile).store(stream, std::memory_order_release); + return true; +} + +extern "C" std::FILE* HostStandardOutputFile_nid_no_patch() { + if (std::FILE* kept = g_hostStandardOutputFile.load(std::memory_order_acquire)) return kept; + return __acrt_iob_func(1); +} + +extern "C" std::FILE* HostStandardErrorFile_nid_no_patch() { + if (std::FILE* kept = g_hostStandardErrorFile.load(std::memory_order_acquire)) return kept; + return __acrt_iob_func(2); +} +#endif + extern "C" void AddPathAlias_nid_no_patch(const char* guestPrefix, const char* hostPath) { if (guestPrefix == nullptr || hostPath == nullptr) { APS5_INVALID_ARG_EX; @@ -548,9 +695,10 @@ extern "C" std::filesystem::path RelinkedModulePath_nid_no_patch(const std::file const auto relinked = RelinkedModuleName(path); std::error_code error; if (std::filesystem::is_regular_file(relinked, error)) return relinked; - NameMatch names[] = {{relinked.filename().string()}}; + NameMatch names[] = {{relinked.filename().string()}, {path.filename().string(), std::nullopt, false, true}}; MatchNames(relinked.parent_path().empty() ? "." : relinked.parent_path(), names); - return MatchedModuleOrPath(path, relinked, names[0]); + if (names[0].ambiguous || names[0].path) return MatchedModuleOrPath(path, relinked, names[0]); + return names[1].path ? *names[1].path : path; } extern "C" int SetWorkingDirectory_nid_no_patch(const char* resolved) { @@ -558,7 +706,7 @@ extern "C" int SetWorkingDirectory_nid_no_patch(const char* resolved) { auto& state = Directories(); std::lock_guard lock(state.mutex); try { - return SetWorkingDirectory(state, std::filesystem::u8path(resolved)); + return SetWorkingDirectory(state, std::filesystem::path(reinterpret_cast(resolved))); } catch (const std::bad_alloc&) { return 12; } catch (const std::filesystem::filesystem_error& error) { @@ -584,7 +732,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; } @@ -640,6 +788,8 @@ extern "C" int WindowsOpenError_nid_no_patch(unsigned long error) { return ENOMEM; case ERROR_DISK_FULL: return ENOSPC; + case ERROR_CANT_RESOLVE_FILENAME: + return ELOOP; default: return EINVAL; } diff --git a/core/libs/prx/libc/src/GuestArena.cpp b/core/libs/prx/libc/src/GuestArena.cpp index 15d799c07e..20cdbc1820 100644 --- a/core/libs/prx/libc/src/GuestArena.cpp +++ b/core/libs/prx/libc/src/GuestArena.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #ifndef _WIN32 #include #include @@ -44,11 +45,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; @@ -108,6 +105,16 @@ public: return 0; } + bool NextUsed(std::uintptr_t address, std::uintptr_t* start, std::uintptr_t* end) { + std::lock_guard lock(_lock); + auto it = _used.upper_bound(address); + if (it != _used.begin() && std::prev(it)->second > address) --it; + if (it == _used.end()) return false; + *start = it->first; + *end = it->second; + return true; + } + bool MarkUsedForCommit(const void* pointer, std::size_t bytes) { std::lock_guard lock(_lock); const auto start = reinterpret_cast(pointer); @@ -364,6 +371,10 @@ std::uintptr_t GuestArenaHighestFree_nid_no_patch(std::size_t bytes, std::size_t return Arena::Get().HighestFree(bytes, alignment); } +bool GuestArenaNextUsed_nid_no_patch(std::uintptr_t address, std::uintptr_t* start, std::uintptr_t* end) { + return Arena::Get().NextUsed(address, start, end); +} + void GuestArenaMarkUsed_nid_postfix(const void* pointer, std::size_t bytes) { Arena::Get().MarkUsed(pointer, bytes); } @@ -462,7 +473,23 @@ void GuestArenaMap_nid_postfix(void* pointer, std::size_t bytes, void* section, void GuestArenaMapDirect_nid_postfix(void* pointer, std::size_t bytes, void* section, std::uint64_t offset, std::uint32_t protection) { if (!Arena::Get().Contains(pointer, bytes)) throw OutsideArena("direct memory mapping", pointer, bytes); - WindowsMappings::Get().Map(pointer, bytes, section, offset, protection, Arena::Get().WriteWatched(), true); + const auto generation = commitGeneration.fetch_add(1, std::memory_order_acq_rel) + 1; + std::vector> created; + std::exception_ptr failure; + try { + WindowsMappings::Get().Map(pointer, bytes, section, offset, protection, Arena::Get().WriteWatched(), true, PAGE_EXECUTE_READWRITE, SIZE_MAX, &created); + } catch (...) { + failure = std::current_exception(); + } + try { + if (const auto callback = privateMappingObserver.load(std::memory_order_acquire)) { + for (const auto& [address, size] : created) callback(address, size, generation); + } + } catch (...) { + if (failure != nullptr) std::rethrow_exception(failure); + throw; + } + if (failure != nullptr) std::rethrow_exception(failure); noteAccess(pointer, bytes); } @@ -486,6 +513,10 @@ bool GuestArenaResidentSpan_nid_postfix(std::uintptr_t address, std::size_t byte void GuestArenaForgetSection_nid_postfix(void* section) { WindowsMappings::Get().ForgetSection(section); } + +bool GuestArenaForgetReusableSection_nid_no_patch(void* section) { + return WindowsMappings::Get().ForgetSection(section); +} #endif #if defined(__linux__) || defined(__APPLE__) @@ -563,6 +594,16 @@ bool GuestArenaBeginHostWrite_nid_postfix(void* pointer, std::size_t bytes) { #endif } +#ifdef _WIN32 +bool GuestArenaBeginHostImport_nid_no_patch(void* pointer, std::size_t bytes) { + return WindowsMappings::Get().BeginHostWrite(reinterpret_cast(pointer), bytes, false); +} + +void GuestArenaEndHostImport_nid_no_patch(void* pointer, std::size_t bytes) { + WindowsMappings::Get().EndHostWrite(reinterpret_cast(pointer), bytes, false); +} +#endif + void GuestArenaEndHostWrite_nid_postfix(void* pointer, std::size_t bytes) { #ifdef _WIN32 WindowsMappings::Get().EndHostWrite(reinterpret_cast(pointer), bytes); 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/LocaleSupport.cpp b/core/libs/prx/libc/src/LocaleSupport.cpp index e2e687de7f..a4388c8dfe 100644 --- a/core/libs/prx/libc/src/LocaleSupport.cpp +++ b/core/libs/prx/libc/src/LocaleSupport.cpp @@ -35,6 +35,11 @@ std::mutex g_localeInitMutex; bool g_localeInitialized = false; std::vector g_registeredFacets; +constexpr std::int64_t StandardStreamSlots = 8; +std::mutex g_standardStreamMutex; +GuestLocale::IosBase* g_standardStreams[StandardStreamSlots + 1] = {}; +int g_standardStreamOpens[StandardStreamSlots + 1] = {}; + void APS5_VABI DestroyClassicLocale(GuestLocale::Facet*) { throw std::runtime_error("Cannot destroy the classic locale"); } @@ -302,7 +307,12 @@ GuestLocale::Implementation* _ZSt21_sceLibcClassicLocale_nid_postfix = &g_classi void APS5_VABI _ZNSt8ios_baseD2Ev_nid_postfix(GuestLocale::IosBase* self) { if (self == nullptr) throw std::invalid_argument("ios_base destructor: null object"); - if (self->standardStream != 0 || self->storage != nullptr || self->callbacks != nullptr) throw std::runtime_error("ios_base destructor: unsupported stream storage or callbacks"); + std::lock_guard lock(g_standardStreamMutex); + if (self->standardStream > 0) { + if (self->standardStream > StandardStreamSlots) throw std::runtime_error("ios_base destructor: unknown standard stream slot"); + if (0 < --g_standardStreamOpens[self->standardStream]) return; + } + if (self->storage != nullptr || self->callbacks != nullptr) throw std::runtime_error("ios_base destructor: unsupported stream storage or callbacks"); if (self->locale != &_ZSt21_sceLibcClassicLocale_nid_postfix) throw std::runtime_error("ios_base destructor: unsupported locale ownership"); self->locale = nullptr; } @@ -324,6 +334,16 @@ StreamManipulator APS5_VABI _ZSt4setwi_nid_postfix(std::int32_t width) { return {&SetStreamWidth, width}; } +void APS5_VABI _ZNSt8ios_base7_AddstdEPS__nid_postfix(GuestLocale::IosBase* self) { + if (self == nullptr) throw std::invalid_argument("ios_base _Addstd: null stream"); + std::lock_guard lock(g_standardStreamMutex); + for (self->standardStream = 0; ++self->standardStream < StandardStreamSlots; ) { + if (g_standardStreams[self->standardStream] == nullptr || g_standardStreams[self->standardStream] == self) break; + } + g_standardStreams[self->standardStream] = self; + ++g_standardStreamOpens[self->standardStream]; +} + // Dinkumware's static locale::_Init(bool) returns the global _Locimp, which the caller copies. GuestLocale::Implementation* APS5_VABI _ZNSt6locale5_InitEv_nid_postfix() { std::lock_guard lock(g_localeInitMutex); diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 8d21db8f88..bfed7a8a4d 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -183,11 +183,31 @@ long double AsinDenominator(long double t) { return 1.0L + t * (q[0] + t * (q[1] + t * (q[2] + t * (q[3] + t * q[4])))); } +template +bool NanWithoutException(T value) { + if constexpr (sizeof(T) == sizeof(std::uint32_t)) { + std::uint32_t bits; + std::memcpy(&bits, &value, sizeof(bits)); + return (bits & UINT32_C(0x7fffffff)) > UINT32_C(0x7f800000); + } else if constexpr (sizeof(T) == sizeof(std::uint64_t)) { + std::uint64_t bits; + std::memcpy(&bits, &value, sizeof(bits)); + return (bits & UINT64_C(0x7fffffffffffffff)) > UINT64_C(0x7ff0000000000000); + } else { + static_assert(std::numeric_limits::digits == 64); + std::uint64_t significand; + std::uint16_t signAndExponent; + std::memcpy(&significand, &value, sizeof(significand)); + std::memcpy(&signAndExponent, reinterpret_cast(&value) + sizeof(significand), sizeof(signAndExponent)); + return (signAndExponent & 0x7fff) == 0x7fff && (significand & UINT64_C(0x7fffffffffffffff)) != 0; + } +} + template T GuestFmin(T x, T y) { - if (std::isnan(x)) + if (NanWithoutException(x)) return y; - if (std::isnan(y)) + if (NanWithoutException(y)) return x; if (std::signbit(x) != std::signbit(y)) return std::signbit(x) ? x : y; @@ -196,15 +216,52 @@ T GuestFmin(T x, T y) { template T GuestFmax(T x, T y) { - if (std::isnan(x)) + if (NanWithoutException(x)) return y; - if (std::isnan(y)) + if (NanWithoutException(y)) return x; if (std::signbit(x) != std::signbit(y)) return std::signbit(x) ? y : x; return x < y ? y : x; } +float GuestBesselSecondKindZero() { + std::feraiseexcept(FE_DIVBYZERO); + return -std::numeric_limits::infinity(); +} + +float ScaledHyperbolic(float x, float y, bool sine) { + const auto xBits = std::bit_cast(x); + const auto xMagnitude = xBits & 0x7fffffffu; + const auto yMagnitude = std::bit_cast(y) & 0x7fffffffu; + constexpr std::uint32_t infinity = 0x7f800000u; + if (xMagnitude > infinity) return x; + if (yMagnitude > infinity) return y; + if ((xMagnitude == infinity && yMagnitude == 0) || (sine && xMagnitude == 0 && yMagnitude == infinity)) { + errno = EDOM; + std::feraiseexcept(FE_INVALID); + return std::numeric_limits::quiet_NaN(); + } + if (xMagnitude == infinity) return sine ? x * y : std::copysign(std::numeric_limits::infinity(), y); + if (xMagnitude == 0 || yMagnitude == 0 || yMagnitude == infinity) return sine ? x * y : y; + constexpr double limit = 2.0 * std::numeric_limits::max_exponent; + const float magnitude = std::bit_cast(xMagnitude); + const double argument = magnitude < limit ? static_cast(magnitude) : limit; + const double value = static_cast(y) * (sine ? std::sinh(argument) : std::cosh(argument)); + const bool exponential = magnitude >= (sine ? 7.7f : 8.675f); + float result; + if (exponential && std::fabs(value) >= 0x1p128) { + result = std::copysign(std::numeric_limits::infinity(), y); + } else { + result = static_cast(value); + } + if (exponential && std::isinf(result)) { + errno = ERANGE; + std::feraiseexcept(FE_OVERFLOW | FE_INEXACT); + } + return sine && (xBits & 0x80000000u) != 0 ? -result : result; +} + } extern "C" { @@ -278,6 +335,7 @@ double APS5_VABI scalbn_nid_postfix(double x, int exponent) { return std::scalbn float APS5_VABI scalbnf_nid_postfix(float x, int exponent) { return std::scalbn(x, exponent); } long double APS5_VABI scalbnl_nid_postfix(long double x, int exponent) { return std::scalbn(x, exponent); } float APS5_VABI significandf_nid_postfix(float x) { return x == 0.0f || !std::isfinite(x) ? x : std::scalbn(x, -std::ilogb(x)); } +double APS5_VABI significand_nid_postfix(double x) { return x == 0.0 || !std::isfinite(x) ? x : std::scalbn(x, -std::ilogb(x)); } double APS5_VABI frexp_nid_postfix(double x, int* exponent) { return std::frexp(x, exponent); } float APS5_VABI frexpf_nid_postfix(float x, int* exponent) { return std::frexp(x, exponent); } // Guest long is 64-bit, including on Windows where native long is 32-bit. @@ -344,6 +402,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); @@ -354,9 +413,9 @@ long double APS5_VABI hypot3l_nid_postfix(long double x, long double y, long dou float APS5_VABI j0f_nid_postfix(float x) { return static_cast(::j0(x)); } float APS5_VABI j1f_nid_postfix(float x) { return static_cast(::j1(x)); } float APS5_VABI jnf_nid_postfix(int order, float x) { return static_cast(::jn(order, x)); } -float APS5_VABI y0f_nid_postfix(float x) { return static_cast(::y0(x)); } -float APS5_VABI y1f_nid_postfix(float x) { return static_cast(::y1(x)); } -float APS5_VABI ynf_nid_postfix(int order, float x) { return static_cast(::yn(order, x)); } +float APS5_VABI y0f_nid_postfix(float x) { return x == 0.0f ? GuestBesselSecondKindZero() : static_cast(::y0(x)); } +float APS5_VABI y1f_nid_postfix(float x) { return x == 0.0f ? GuestBesselSecondKindZero() : static_cast(::y1(x)); } +float APS5_VABI ynf_nid_postfix(int order, float x) { return x == 0.0f ? GuestBesselSecondKindZero() : static_cast(::yn(order, x)); } long double APS5_VABI asinl_nid_postfix(long double x) { const ExtendedParts parts = SplitExtended(x); @@ -528,6 +587,7 @@ double APS5_VABI round_nid_postfix(double x) { return std::round(x); } float APS5_VABI cbrtf_nid_postfix(float x) { return std::cbrt(x); } float APS5_VABI remainderf_nid_postfix(float x, float y) { return std::remainder(x, y); } float APS5_VABI nextafterf_nid_postfix(float x, float y) { return std::nextafter(x, y); } +double APS5_VABI nextafter_nid_postfix(double x, double y) { return std::nextafter(x, y); } int APS5_VABI __isfinite_nid_postfix(double x) { return std::isfinite(x) ? 1 : 0; } int APS5_VABI __isnan_nid_postfix(double x) { return std::isnan(x) ? 1 : 0; } int APS5_VABI __isinf_nid_postfix(double x) { return std::isinf(x) ? 1 : 0; } @@ -575,8 +635,8 @@ int APS5_VABI ilogbf_nid_postfix(float x) { return ilogb_nid_postfix(static_cast double APS5_VABI remainder_nid_postfix(double x, double y) { return std::remainder(x, y); } double APS5_VABI remquo_nid_postfix(double x, double y, int* quo) { return std::remquo(x, y, quo); } float APS5_VABI remquof_nid_postfix(float x, float y, int* quo) { return std::remquo(x, y, quo); } -float APS5_VABI _FSinh_nid_postfix(float x, float y) { return y * std::sinh(x); } -float APS5_VABI _FCosh_nid_postfix(float x, float y) { return y * std::cosh(x); } +float APS5_VABI _FSinh_nid_postfix(float x, float y) { return ScaledHyperbolic(x, y, true); } +float APS5_VABI _FCosh_nid_postfix(float x, float y) { return ScaledHyperbolic(x, y, false); } struct alignas(16) LibcFloatConstant { std::uint32_t bits[4]; }; LibcFloatConstant _FInf_nid_postfix {{0x7f800000u, 0, 0, 0}}; @@ -633,6 +693,26 @@ int APS5_VABI __isfinitel_nid_postfix(long double x) { return (exponent & 0x7fffu) != 0x7fffu; } +struct DinkumwareFloatCharacteristics { + int digits; + int mantissaDigits; + int maxExponent10; + int maxExponent; + int minExponent10; + int minExponent; + alignas(16) std::uint16_t epsilon[8]; + alignas(16) std::uint16_t max[8]; + alignas(16) std::uint16_t min[8]; +}; +static_assert(sizeof(DinkumwareFloatCharacteristics) == 80 && offsetof(DinkumwareFloatCharacteristics, epsilon) == 32); + +DinkumwareFloatCharacteristics _Flt_nid_postfix {6, 24, 38, 128, -37, -125, + {0, 0x3400}, {0xffff, 0x7f7f}, {0, 0x0080}}; +DinkumwareFloatCharacteristics _Dbl_nid_postfix {15, 53, 308, 1024, -307, -1021, + {0, 0, 0, 0x3cb0}, {0xffff, 0xffff, 0xffff, 0x7fef}, {0, 0, 0, 0x0010}}; +DinkumwareFloatCharacteristics _Ldbl_nid_postfix {18, 64, 4932, 16384, -4931, -16381, + {0, 0, 0, 0x8000, 0x3fc0}, {0xffff, 0xffff, 0xffff, 0xffff, 0x7ffe}, {0, 0, 0, 0x8000, 0x0001}}; + static std::mutex g_randLock; static std::uint32_t g_randState = 1; @@ -699,4 +779,14 @@ union SonyDconst { static_assert(std::endian::native == std::endian::little); SonyDconst _Inf_nid_postfix {{0, 0, 0, 0x7ff0, 0, 0, 0, 0}}; +SonyDconst _Denorm_nid_postfix {{1, 0, 0, 0, 0, 0, 0, 0}}; +SonyDconst _Hugeval_nid_postfix {{0, 0, 0, 0x7ff0, 0, 0, 0, 0}}; +SonyDconst _Nan_nid_postfix {{0, 0, 0, 0x7ff8, 0, 0, 0, 0}}; +SonyDconst _Snan_nid_postfix {{1, 0, 0, 0x7ff0, 0, 0, 0, 0}}; +LibcFloatConstant _FDenorm_nid_postfix {{0x00000001u, 0, 0, 0}}; +LibcFloatConstant _FSnan_nid_postfix {{0x7f800001u, 0, 0, 0}}; +SonyDconst _LDenorm_nid_postfix {{1, 0, 0, 0, 0, 0, 0, 0}}; +SonyDconst _LInf_nid_postfix {{0, 0, 0, 0x8000, 0x7fff, 0, 0, 0}}; +SonyDconst _LNan_nid_postfix {{0, 0, 0, 0xc000, 0x7fff, 0, 0, 0}}; +SonyDconst _LSnan_nid_postfix {{1, 0, 0, 0x8000, 0x7fff, 0, 0, 0}}; } diff --git a/core/libs/prx/libc/src/Mspace.cpp b/core/libs/prx/libc/src/Mspace.cpp index f2c91e33a4..8830e5c75c 100644 --- a/core/libs/prx/libc/src/Mspace.cpp +++ b/core/libs/prx/libc/src/Mspace.cpp @@ -314,6 +314,11 @@ void* APS5_VABI sceLibcPafMspaceMalloc_nid_postfix(void* handle, std::size_t siz return sceLibcMspaceMalloc_nid_postfix(handle, size); } +void* APS5_VABI sceLibcPafMspaceMemalign_nid_postfix(void* handle, std::size_t alignment, std::size_t size, const void* owner) { + (void)owner; + return sceLibcMspaceMemalign_nid_postfix(handle, alignment, size); +} + void APS5_VABI sceLibcPafMspaceFree_nid_postfix(void* handle, void* pointer, const void* owner) { (void)owner; sceLibcMspaceFree_nid_postfix(handle, pointer); @@ -334,6 +339,11 @@ void* APS5_VABI sceLibcPafMspaceReallocalign_nid_postfix(void* handle, void* poi return sceLibcMspaceReallocalign_nid_postfix(handle, pointer, alignment, size); } +int APS5_VABI sceLibcPafMspacePosixMemalign_nid_postfix(void* handle, void** result, std::size_t alignment, std::size_t size, const void* owner) { + (void)owner; + return sceLibcMspacePosixMemalign_nid_postfix(handle, result, alignment, size); +} + int APS5_VABI sceLibcPafMspaceMallocStats_nid_postfix(void* handle, MallocManagedSize* stats) { return sceLibcMspaceMallocStats_nid_postfix(handle, stats); } diff --git a/core/libs/prx/libc/src/Multibyte.cpp b/core/libs/prx/libc/src/Multibyte.cpp index 5865fe5933..e7dc400ac3 100644 --- a/core/libs/prx/libc/src/Multibyte.cpp +++ b/core/libs/prx/libc/src/Multibyte.cpp @@ -1,5 +1,6 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/SonyCrt.hpp" +#include #include #include #include @@ -303,6 +304,13 @@ int APS5_VABI wcsrtombs_s_nid_postfix( return 0; } +int APS5_VABI wcstombs_s_nid_postfix( + std::size_t* result, char* destination, std::size_t capacity, const std::uint16_t* source, std::size_t limit +) { + std::array state{}; + return wcsrtombs_s_nid_postfix(result, destination, capacity, &source, limit, state.data()); +} + std::size_t APS5_VABI mbsrtowcs_nid_postfix(std::uint16_t* destination, const char** source, std::size_t capacity, void*) { if (!destination) return std::strlen(*source); std::size_t converted = 0; diff --git a/core/libs/prx/libc/src/Process.cpp b/core/libs/prx/libc/src/Process.cpp index e6d58a919f..9d2f6909b0 100644 --- a/core/libs/prx/libc/src/Process.cpp +++ b/core/libs/prx/libc/src/Process.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -100,6 +101,7 @@ extern "C" void LibcRunShutdown_nid_postfix() { namespace { std::atomic endFlush{nullptr}; +std::atomic exitDrain{nullptr}; void (*previousAbortHandler)(int) = SIG_DFL; void FlushOnAbort(int number) { @@ -129,6 +131,12 @@ extern "C" void LibcRunEndFlush_nid_no_patch() { if (const auto flush = endFlush.load(std::memory_order_acquire)) flush(); } +extern "C" void LibcSetExitDrain_nid_no_patch(void (*drain)()) { + if (drain == nullptr) throw std::runtime_error("libc: invalid exit drain registration"); + void (*registered)() = nullptr; + if (!exitDrain.compare_exchange_strong(registered, drain) && registered != drain) throw std::runtime_error("libc: exit drain already registered"); +} + namespace { void StartExit() { @@ -154,6 +162,26 @@ extern "C" { } [[noreturn]] void LibcTerminate_nid_no_patch(int code) { + if (const auto drain = exitDrain.exchange(nullptr)) { + try { + auto completion = std::make_shared>(); + auto finished = completion->get_future(); + std::thread([completion, drain] { + try { + drain(); + completion->set_value(); + } catch (...) { + completion->set_exception(std::current_exception()); + } + }).detach(); + if (finished.wait_for(std::chrono::seconds(8)) == std::future_status::ready) finished.get(); + else std::fputs("[libc] host exit drain exceeded 8 seconds\n", stderr); + } catch (const std::exception& error) { + std::fprintf(stderr, "[libc] host exit drain failed: %s\n", error.what()); + } catch (...) { + std::fputs("[libc] host exit drain failed\n", stderr); + } + } LibcRunEndFlush_nid_no_patch(); #ifdef _WIN32 TerminateProcess(GetCurrentProcess(), static_cast(code)); diff --git a/core/libs/prx/libc/src/Random.cpp b/core/libs/prx/libc/src/Random.cpp index 787fe00b2b..06cf1d789d 100644 --- a/core/libs/prx/libc/src/Random.cpp +++ b/core/libs/prx/libc/src/Random.cpp @@ -126,7 +126,7 @@ std::int64_t APS5_VABI nrand48_nid_postfix(std::uint16_t* seed) { } std::int64_t APS5_VABI jrand48_nid_postfix(std::uint16_t* seed) { - return static_cast(static_cast(NextRand48(seed, __func__) >> 16)); + return static_cast(NextRand48(seed, __func__) >> 16); } double APS5_VABI drand48_nid_postfix() { @@ -141,7 +141,7 @@ std::int64_t APS5_VABI lrand48_nid_postfix() { std::int64_t APS5_VABI mrand48_nid_postfix() { std::lock_guard lock(g_randomLock); - return static_cast(static_cast(NextRand48(g_rand48Seed, __func__) >> 16)); + return static_cast(NextRand48(g_rand48Seed, __func__) >> 16); } void APS5_VABI srand48_nid_postfix(std::int64_t seedval) { diff --git a/core/libs/prx/libc/src/Rename.cpp b/core/libs/prx/libc/src/Rename.cpp index f5d9545b5c..973c142ff6 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 @@ -93,13 +94,32 @@ static int RenamePath_nid_no_patch(const std::filesystem::path& source, const st return error ? FilesystemError(error) : 0; } +static int SourceLookupError_nid_no_patch(const std::filesystem::path& source) { + std::error_code status; + const auto sourceStatus = std::filesystem::symlink_status(source, status); + if (HasNonDirectoryPrefix_nid_no_patch(source)) return 20; + if (status && status != std::errc::no_such_file_or_directory) return FilesystemError(status); + return sourceStatus.type() == std::filesystem::file_type::not_found ? 2 : 0; +} + 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 (!from) { errno = 14; return -1; } + if (!*from) { errno = 2; return -1; } + if (const int dot = RenameDotError_nid_no_patch(from)) { errno = dot; return -1; } + const int destinationError = !to ? 14 : !*to ? 2 : RenameDotError_nid_no_patch(to); try { const auto source = ResolvePath_nid_no_patch(from); + if (destinationError) { + const int sourceError = SourceLookupError_nid_no_patch(source); + errno = sourceError ? sourceError : destinationError; + return -1; + } 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/RuneLocale.cpp b/core/libs/prx/libc/src/RuneLocale.cpp index 787200a281..fa25f0412c 100644 --- a/core/libs/prx/libc/src/RuneLocale.cpp +++ b/core/libs/prx/libc/src/RuneLocale.cpp @@ -154,4 +154,34 @@ int APS5_VABI wctob_nid_postfix(std::uint32_t c) { return c < 256 ? static_cast(c) : -1; } +std::uint32_t APS5_VABI _Towctrans_nid_postfix(std::uint32_t c, std::uint64_t desc); +const short* APS5_VABI _Getptolower_nid_postfix(); +const short* APS5_VABI _Getptoupper_nid_postfix(); + +std::uint64_t APS5_VABI wctrans_nid_postfix(const char* property) { + if (std::strcmp(property, "tolower") == 0) return 1; + if (std::strcmp(property, "toupper") == 0) return 2; + return 0; +} + +std::uint32_t APS5_VABI towctrans_nid_postfix(std::uint32_t c, std::uint64_t desc) { + return desc == 1 || desc == 2 ? _Towctrans_nid_postfix(c, desc) : c; +} + +std::uint32_t APS5_VABI _Towlower_nid_postfix(std::uint32_t c, const void*) { + return _Towctrans_nid_postfix(c, 1); +} + +std::uint32_t APS5_VABI _Towupper_nid_postfix(std::uint32_t c, const void*) { + return _Towctrans_nid_postfix(c, 2); +} + +int APS5_VABI _Tolower_nid_postfix(int c, const void*) { + return static_cast(c) <= 0x7f ? _Getptolower_nid_postfix()[c] : c; +} + +int APS5_VABI _Toupper_nid_postfix(int c, const void*) { + return static_cast(c) <= 0x7f ? _Getptoupper_nid_postfix()[c] : c; +} + } diff --git a/core/libs/prx/libc/src/RuntimeSupport.cpp b/core/libs/prx/libc/src/RuntimeSupport.cpp index 97995bf9d2..f7204ea7f1 100644 --- a/core/libs/prx/libc/src/RuntimeSupport.cpp +++ b/core/libs/prx/libc/src/RuntimeSupport.cpp @@ -57,6 +57,10 @@ FileStream _Stderr_nid_postfix{stderr}; FileStream _Stdout_nid_postfix{stdout}; FileStream _Stdin_nid_postfix{stdin}; +std::FILE* GuestStandardOutputFile_nid_no_patch() { + return GetNativeStream(&_Stdout_nid_postfix); +} + int APS5_VABI __cxa_atexit_nid_postfix(void (APS5_VABI *func)(void*), void* arg, void* dsoHandle) { std::lock_guard lock(g_exitMutex); g_exitDestructors.push_back({func, arg, dsoHandle}); @@ -113,12 +117,12 @@ unsigned int APS5_VABI _Atomic_load_4_nid_postfix(volatile unsigned int* target, } std::intmax_t APS5_VABI strtoimax_nid_postfix(const char* str, char** endptr, int base) { - if (StopAtPrefixLetter_nid_no_patch(str, endptr, base)) return 0; + if (RejectIntegerBase_nid_no_patch(str, endptr, base, false) || StopAtPrefixLetter_nid_no_patch(str, endptr, base)) return 0; return ParseInteger(str, endptr, base, std::strtoimax); } std::uintmax_t APS5_VABI strtoumax_nid_postfix(const char* str, char** endptr, int base) { - if (StopAtPrefixLetter_nid_no_patch(str, endptr, base)) return 0; + if (RejectIntegerBase_nid_no_patch(str, endptr, base, false) || StopAtPrefixLetter_nid_no_patch(str, endptr, base)) return 0; return ParseInteger(str, endptr, base, std::strtoumax); } @@ -168,7 +172,7 @@ int APS5_VABI ftrylockfile_nid_postfix(FileStream* stream) { } std::uint64_t APS5_VABI _Stoul_nid_postfix(const char* str, char** endptr, int base) { - if (StopAtPrefixLetter_nid_no_patch(str, endptr, base)) return 0; + if (RejectIntegerBase_nid_no_patch(str, endptr, base, false) || StopAtPrefixLetter_nid_no_patch(str, endptr, base)) return 0; return ParseInteger(str, endptr, base, std::strtoull); } @@ -196,8 +200,4 @@ __int128 APS5_VABI __modti3_nid_postfix(__int128 dividend, __int128 divisor) { return SonyModti3(dividend, divisor); } -void APS5_VABI _ZNSt8ios_base7_AddstdEPS__nid_postfix(void*) { - NotImplemented_nid_no_patch(__func__); -} - } diff --git a/core/libs/prx/libc/src/ScanWideField.cpp b/core/libs/prx/libc/src/ScanWideField.cpp new file mode 100644 index 0000000000..5d5a35559d --- /dev/null +++ b/core/libs/prx/libc/src/ScanWideField.cpp @@ -0,0 +1,68 @@ +#include "prx/libc/include/ScanWideField.hpp" +#include "prx/libc/include/General.hpp" + +#include + +namespace { + +std::size_t DecodeUtf8(const char* text, char32_t* point) { + const auto lead = static_cast(text[0]); + if (lead < 0x80) { + *point = lead; + return 1; + } + std::size_t size = 0; + char32_t value = 0; + if ((lead & 0xE0) == 0xC0) { size = 2; value = lead & 0x1F; } + else if ((lead & 0xF0) == 0xE0) { size = 3; value = lead & 0x0F; } + else if ((lead & 0xF8) == 0xF0) { size = 4; value = lead & 0x07; } + else return 0; + for (std::size_t index = 1; index < size; ++index) { + const auto next = static_cast(text[index]); + if ((next & 0xC0) != 0x80) return 0; + value = (value << 6) | (next & 0x3F); + } + constexpr char32_t Smallest[] = {0, 0, 0x80, 0x800, 0x10000}; + if (value < Smallest[size] || value > 0x10FFFF || (value >= 0xD800 && value <= 0xDFFF)) return 0; + *point = value; + return size; +} + +} + +namespace LibcDetail { + +std::size_t ReadWideScanCharacter(const char* text, char32_t* point) { + const std::size_t size = DecodeUtf8(text, point); + if (size == 0) NotImplemented_nid_no_patch("scanf 16-bit wide conversion of input that is not UTF-8"); + if (*point > 0xFFFF) NotImplemented_nid_no_patch("scanf 16-bit wide conversion of a character outside the BMP"); + return size; +} + +bool InWideScanset(const std::string& specifier, char32_t point) { + std::size_t index = 1; + const bool negated = specifier[index] == '^'; + if (negated) ++index; + const std::size_t end = specifier.size() - 1; + bool member = false; + for (std::size_t first = index; index < end; ++index) { + const auto value = static_cast(specifier[index]); + if (value >= 0x80) NotImplemented_nid_no_patch("scanf 16-bit wide scanset with a character outside ASCII"); + if (specifier[index] == '-' && index != first && index + 1 < end) { + const auto low = static_cast(specifier[index - 1]); + const auto high = static_cast(specifier[index + 1]); + if (high >= 0x80) NotImplemented_nid_no_patch("scanf 16-bit wide scanset with a character outside ASCII"); + if (point >= low && point <= high) member = true; + ++index; + continue; + } + if (point == value) member = true; + } + return member != negated; +} + +void RequireNoPositionalWideScan(const char* format) { + if (std::strchr(format, '$') != nullptr) NotImplemented_nid_no_patch("sscanf positional arguments with a 16-bit wide conversion on Linux"); +} + +} diff --git a/core/libs/prx/libc/src/ScanningWide.cpp b/core/libs/prx/libc/src/ScanningWide.cpp index e03bf6a2d4..0b95de6e78 100644 --- a/core/libs/prx/libc/src/ScanningWide.cpp +++ b/core/libs/prx/libc/src/ScanningWide.cpp @@ -10,6 +10,7 @@ #include #include "SceTypes.hpp" +#include "prx/libc/include/FileStream.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/WindowsFormatting.hpp" @@ -23,6 +24,10 @@ extern "C" int APS5_VABI iswspace_nid_postfix(std::uint32_t c); extern "C" std::size_t APS5_VABI wcrtomb_nid_postfix(char* destination, std::uint16_t value, void* state); +extern "C" std::int32_t APS5_VABI fgetwc_nid_postfix(FileStream* stream); +extern "C" int APS5_VABI ungetc_nid_postfix(int value, FileStream* stream); +extern "C" void APS5_VABI flockfile_nid_postfix(FileStream* stream); +extern "C" void APS5_VABI funlockfile_nid_postfix(FileStream* stream); namespace { @@ -130,21 +135,92 @@ Fit FloatFit(std::string_view text) { return Fit::Valid; } +class WideInput { + const char16_t* const text; + FileStream* const stream; + std::int32_t next = 0; + bool fetched = false; + std::int64_t consumed = 0; + +public: + explicit WideInput(const char16_t* buffer) : text(buffer), stream(nullptr) {} + explicit WideInput(FileStream* source) : text(nullptr), stream(source) {} + + std::int32_t Peek() { + if (!fetched) { + next = stream != nullptr ? fgetwc_nid_postfix(stream) : text[consumed] != 0 ? text[consumed] : -1; + fetched = true; + } + return next; + } + + void Advance() { + fetched = false; + ++consumed; + } + + std::int64_t Consumed() const { + return consumed; + } + + void Release() { + if (!fetched || stream == nullptr || next < 0) return; + fetched = false; + const std::uint32_t code = static_cast(next); + unsigned char bytes[4]; + std::size_t count = 0; + if (code < 0x80) { + bytes[count++] = static_cast(code); + } else if (code < 0x800) { + bytes[count++] = static_cast(0xc0 | (code >> 6)); + bytes[count++] = static_cast(0x80 | (code & 0x3f)); + } else if (code < 0x10000) { + bytes[count++] = static_cast(0xe0 | (code >> 12)); + bytes[count++] = static_cast(0x80 | ((code >> 6) & 0x3f)); + bytes[count++] = static_cast(0x80 | (code & 0x3f)); + } else { + bytes[count++] = static_cast(0xf0 | (code >> 18)); + bytes[count++] = static_cast(0x80 | ((code >> 12) & 0x3f)); + bytes[count++] = static_cast(0x80 | ((code >> 6) & 0x3f)); + bytes[count++] = static_cast(0x80 | (code & 0x3f)); + } + while (count != 0) ungetc_nid_postfix(bytes[--count], stream); + } +}; + +class LockedStream { + FileStream* const stream; + +public: + explicit LockedStream(FileStream* value) : stream(value->IsOpen() ? value : nullptr) { + if (stream != nullptr) flockfile_nid_postfix(stream); + } + ~LockedStream() { + if (stream != nullptr) funlockfile_nid_postfix(stream); + } +}; + class WideScanner { const char* const name; const bool sized; - const char16_t* const start; - const char16_t* input; + WideInput input; const char16_t* format; LibcDetail::FormatArguments args; int assigned = 0; bool converted = false; public: - WideScanner(const char* function, bool withSizes, const char16_t* buffer, const char16_t* formatText, const void* source) - : name(function), sized(withSizes), start(buffer), input(buffer), format(formatText), args(source) {} + WideScanner(const char* function, bool withSizes, WideInput source, const char16_t* formatText, const void* arguments) + : name(function), sized(withSizes), input(source), format(formatText), args(arguments) {} int Run() { + const int result = Scan(); + input.Release(); + return result; + } + +private: + int Scan() { while (*format != 0) { Outcome outcome = Outcome::Done; if (IsSpace(*format)) { @@ -163,7 +239,6 @@ public: return assigned; } -private: [[noreturn]] void Fail(const std::string& message) const { throw std::invalid_argument(std::string(name) + ": " + message); } @@ -177,13 +252,13 @@ private: } void SkipSpace() { - while (*input != 0 && IsSpace(*input)) ++input; + while (input.Peek() >= 0 && IsSpace(static_cast(input.Peek()))) input.Advance(); } Outcome Literal(char16_t expected) { - if (*input == 0) return Outcome::Input; - if (*input != expected) return Outcome::Matching; - ++input; + if (input.Peek() < 0) return Outcome::Input; + if (input.Peek() != expected) return Outcome::Matching; + input.Advance(); return Outcome::Done; } @@ -238,6 +313,7 @@ private: case u'u': return Integer(directive, 10, false); case u'x': case u'X': return Integer(directive, 16, false); + case u'p': return Pointer(directive); case u'a': case u'A': case u'e': case u'E': case u'f': case u'F': case u'g': case u'G': return Float(directive); case u's': @@ -249,30 +325,33 @@ private: } template - std::string Field(std::size_t width, TFit fit) const { + std::string Field(std::size_t width, TFit fit) { std::string field; const std::size_t limit = width == 0 ? std::numeric_limits::max() : width; while (field.size() < limit) { - const char16_t next = input[field.size()]; - if (next == 0 || next >= 0x80) break; + const std::int32_t next = input.Peek(); + if (next < 0 || next >= 0x80) break; field.push_back(static_cast(next)); if (fit(field) == Fit::Invalid) { field.pop_back(); break; } + input.Advance(); } return field; } Outcome Integer(const Directive& directive, int base, bool isSigned) { - const std::size_t size = IntegerSize(directive.length); + return Integer(directive, base, isSigned, IntegerSize(directive.length)); + } + + Outcome Integer(const Directive& directive, int base, bool isSigned, std::size_t size) { void* target = nullptr; if (!directive.suppress && (target = args.Next()) == nullptr) return Outcome::Violation; SkipSpace(); - if (*input == 0) return Outcome::Input; + if (input.Peek() < 0) return Outcome::Input; const std::string field = Field(directive.width, [base](std::string_view text) { return IntegerFit(text, base); }); if (IntegerFit(field, base) != Fit::Valid) return Outcome::Matching; - input += field.size(); if (target != nullptr) { const std::uint64_t value = isSigned ? static_cast(std::strtoll(field.c_str(), nullptr, base)) : std::strtoull(field.c_str(), nullptr, base); std::memcpy(target, &value, size); @@ -282,6 +361,11 @@ private: return Outcome::Done; } + Outcome Pointer(const Directive& directive) { + if (!directive.length.empty()) Fail("invalid pointer length modifier " + directive.length); + return Integer(directive, 16, false, sizeof(void*)); + } + Outcome Float(const Directive& directive) { static_assert(sizeof(long double) == 16 && std::numeric_limits::digits == 64); if (!directive.length.empty() && directive.length != "l" && directive.length != "L") @@ -289,10 +373,9 @@ private: void* target = nullptr; if (!directive.suppress && (target = args.Next()) == nullptr) return Outcome::Violation; SkipSpace(); - if (*input == 0) return Outcome::Input; + if (input.Peek() < 0) return Outcome::Input; const std::string field = Field(directive.width, FloatFit); if (FloatFit(field) != Fit::Valid) return Outcome::Matching; - input += field.size(); if (target != nullptr) { if (directive.length == "L") { const long double value = std::strtold(field.c_str(), nullptr); @@ -335,22 +418,25 @@ private: if (target == nullptr) return Outcome::Violation; } if (directive.conversion == u's') SkipSpace(); - if (*input == 0) return Outcome::Input; + if (input.Peek() < 0) return Outcome::Input; std::u16string item; if (directive.conversion == u'c') { const std::size_t count = directive.width == 0 ? 1 : directive.width; - while (item.size() < count && input[item.size()] != 0) item.push_back(input[item.size()]); + while (item.size() < count && input.Peek() >= 0) { + item.push_back(static_cast(input.Peek())); + input.Advance(); + } if (item.size() < count) return Outcome::Matching; } else { const std::size_t limit = directive.width == 0 ? std::numeric_limits::max() : directive.width; - while (item.size() < limit && input[item.size()] != 0) { - const char16_t next = input[item.size()]; + while (item.size() < limit && input.Peek() >= 0) { + const char16_t next = static_cast(input.Peek()); if (directive.conversion == u's' ? IsSpace(next) : !InSet(directive, next)) break; item.push_back(next); + input.Advance(); } if (item.empty()) return Outcome::Matching; } - input += item.size(); if (target == nullptr) { converted = true; return Outcome::Done; @@ -382,7 +468,7 @@ private: const std::size_t size = IntegerSize(directive.length); void* target = args.Next(); if (target == nullptr) return Outcome::Violation; - const std::int64_t count = input - start; + const std::int64_t count = input.Consumed(); std::memcpy(target, &count, size); return Outcome::Done; } @@ -396,7 +482,7 @@ int APS5_VABI swscanf_nid_postfix(const char16_t* buffer, const char16_t* format if (buffer == nullptr || format == nullptr) return EOF; APS5_VA_BEGIN(format); try { - const int result = WideScanner("swscanf", false, buffer, format, args).Run(); + const int result = WideScanner("swscanf", false, WideInput(buffer), format, args).Run(); APS5_VA_END(); return result; } catch (...) { @@ -409,7 +495,87 @@ int APS5_VABI swscanf_s_nid_postfix(const char16_t* buffer, const char16_t* form if (buffer == nullptr || format == nullptr) return EOF; APS5_VA_BEGIN(format); try { - const int result = WideScanner("swscanf_s", true, buffer, format, args).Run(); + const int result = WideScanner("swscanf_s", true, WideInput(buffer), format, args).Run(); + APS5_VA_END(); + return result; + } catch (...) { + APS5_VA_END(); + throw; + } +} + +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, WideInput(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, WideInput(buffer), format, args).Run(); +} + +extern FileStream* __stdinp_nid_postfix; + +int APS5_VABI vfwscanf_nid_postfix(FileStream* stream, const char16_t* format, VaList* args) { + if (stream == nullptr || format == nullptr) return EOF; + const LockedStream lock(stream); + return WideScanner("vfwscanf", false, WideInput(stream), format, args).Run(); +} + +int APS5_VABI vfwscanf_s_nid_postfix(FileStream* stream, const char16_t* format, VaList* args) { + if (stream == nullptr || format == nullptr) return EOF; + const LockedStream lock(stream); + return WideScanner("vfwscanf_s", true, WideInput(stream), format, args).Run(); +} + +int APS5_VABI vwscanf_nid_postfix(const char16_t* format, VaList* args) { + return vfwscanf_nid_postfix(__stdinp_nid_postfix, format, args); +} + +int APS5_VABI vwscanf_s_nid_postfix(const char16_t* format, VaList* args) { + return vfwscanf_s_nid_postfix(__stdinp_nid_postfix, format, args); +} + +int APS5_VABI fwscanf_nid_postfix(FileStream* stream, const char16_t* format, ...) { + APS5_VA_BEGIN(format); + try { + const int result = vfwscanf_nid_postfix(stream, format, reinterpret_cast(args)); + APS5_VA_END(); + return result; + } catch (...) { + APS5_VA_END(); + throw; + } +} + +int APS5_VABI fwscanf_s_nid_postfix(FileStream* stream, const char16_t* format, ...) { + APS5_VA_BEGIN(format); + try { + const int result = vfwscanf_s_nid_postfix(stream, format, reinterpret_cast(args)); + APS5_VA_END(); + return result; + } catch (...) { + APS5_VA_END(); + throw; + } +} + +int APS5_VABI wscanf_nid_postfix(const char16_t* format, ...) { + APS5_VA_BEGIN(format); + try { + const int result = vfwscanf_nid_postfix(__stdinp_nid_postfix, format, reinterpret_cast(args)); + APS5_VA_END(); + return result; + } catch (...) { + APS5_VA_END(); + throw; + } +} + +int APS5_VABI wscanf_s_nid_postfix(const char16_t* format, ...) { + APS5_VA_BEGIN(format); + try { + const int result = vfwscanf_s_nid_postfix(__stdinp_nid_postfix, format, reinterpret_cast(args)); APS5_VA_END(); return result; } catch (...) { diff --git a/core/libs/prx/libc/src/StandardStreams.cpp b/core/libs/prx/libc/src/StandardStreams.cpp index b50bc75eb7..03cdaed4cd 100644 --- a/core/libs/prx/libc/src/StandardStreams.cpp +++ b/core/libs/prx/libc/src/StandardStreams.cpp @@ -35,7 +35,9 @@ int __isthreaded_nid_postfix = 1; int APS5_VABI fgetc_nid_postfix(FileStream* stream) { if (stream && !stream->IsOpen()) { errno = 9; return -1; } - const int result = std::fgetc(GetNativeStream(stream)); + auto* native = GetNativeStream(stream); + if (!stream->SwitchToReading()) return -1; + const int result = std::fgetc(native); stream->SyncStatus(); return result; } @@ -75,17 +77,21 @@ std::int32_t APS5_VABI ungetwc_nid_postfix(std::int32_t value, FileStream* strea stream->SetError(); return -1; } - const int result = std::ungetc(value, GetNativeStream(stream)); + auto* native = GetNativeStream(stream); + if (!stream->SwitchToReading()) return -1; + const int result = std::ungetc(value, native); stream->SyncStatus(); return result; } int APS5_VABI getc_nid_postfix(FileStream* stream) { return fgetc_nid_postfix(stream); } int APS5_VABI getc_unlocked_nid_postfix(FileStream* stream) { if (stream && !stream->IsOpen()) { errno = 9; return -1; } + auto* native = GetNativeStream(stream); + if (!stream->SwitchToReadingUnlocked()) return -1; #ifdef _WIN32 - const int result = _fgetc_nolock(GetNativeStream(stream)); + const int result = _fgetc_nolock(native); #else - const int result = ::getc_unlocked(GetNativeStream(stream)); + const int result = ::getc_unlocked(native); #endif stream->SyncUnlockedStatus(); return result; @@ -117,7 +123,9 @@ int APS5_VABI putchar_nid_postfix(int value) { return fputc_nid_postfix(value, _ int APS5_VABI putchar_unlocked_nid_postfix(int value) { return putc_unlocked_nid_postfix(value, __stdoutp_nid_postfix); } int APS5_VABI ungetc_nid_postfix(int value, FileStream* stream) { if (stream && !stream->IsOpen()) { errno = 9; return -1; } - const int result = std::ungetc(value, GetNativeStream(stream)); + auto* native = GetNativeStream(stream); + if (value != EOF && !stream->SwitchToReading()) return -1; + const int result = std::ungetc(value, native); stream->SyncStatus(); return result; } @@ -128,7 +136,9 @@ char* APS5_VABI fgets_nid_postfix(char* buffer, int size, FileStream* stream) { errno = 22; return nullptr; } - auto* result = std::fgets(buffer, size, GetNativeStream(stream)); + auto* native = GetNativeStream(stream); + if (!stream->SwitchToReading()) return nullptr; + auto* result = std::fgets(buffer, size, native); stream->SyncStatus(); return result; } @@ -140,6 +150,16 @@ int APS5_VABI ferror_nid_postfix(FileStream* stream) { stream->SyncStatus(); return (stream->GuestState().flags & 0x40) != 0; } + +int APS5_VABI feof_unlocked_nid_postfix(FileStream* stream) { + stream->SyncUnlockedStatus(); + return (stream->GuestState().flags & 0x20) != 0; +} + +int APS5_VABI ferror_unlocked_nid_postfix(FileStream* stream) { + stream->SyncUnlockedStatus(); + return (stream->GuestState().flags & 0x40) != 0; +} void APS5_VABI clearerr_nid_postfix(FileStream* stream) { stream->ClearError(); } diff --git a/core/libs/prx/libc/src/StringExtensions.cpp b/core/libs/prx/libc/src/StringExtensions.cpp index f1f4eb975d..18b9cb6f18 100644 --- a/core/libs/prx/libc/src/StringExtensions.cpp +++ b/core/libs/prx/libc/src/StringExtensions.cpp @@ -3,24 +3,51 @@ #include #include #include +#include +#include +#include #include extern "C" { -char* APS5_VABI basename_nid_postfix(const char* path) { - thread_local char buffer[1024]; +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_r_nid_postfix(const char* path, char* buffer) { + if (buffer == nullptr) throw std::invalid_argument("basename_r: null buffer"); std::string_view name = path && *path ? path : "."; while (name.size() > 1 && name.back() == '/') name.remove_suffix(1); if (name != "/") { const auto separator = name.find_last_of('/'); if (separator != std::string_view::npos) name.remove_prefix(separator + 1); } - if (name.size() >= sizeof(buffer)) { errno = 63; return nullptr; } + if (name.size() >= 1024) { errno = 63; return nullptr; } std::memcpy(buffer, name.data(), name.size()); buffer[name.size()] = '\0'; return buffer; } +char* APS5_VABI basename_nid_postfix(const char* path) { + thread_local char buffer[1024]; + return basename_r_nid_postfix(path, buffer); +} + char* APS5_VABI __inet_ntoa_nid_postfix(unsigned int address) { thread_local char buffer[sizeof("255.255.255.255")]; unsigned char bytes[sizeof(address)]; diff --git a/core/libs/prx/libc/src/Strings.cpp b/core/libs/prx/libc/src/Strings.cpp index 40c192a30d..55ee19e5b1 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,18 @@ 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 int saved = errno; const TFloat value = parse(text, &parsedEnd); + if (errno == ERANGE && value != 0 && (value < 0 ? -value : value) < std::numeric_limits::min()) errno = saved; if (end != nullptr) *end = parsedEnd; - return parsedEnd == text ? TFloat{0} : QuietNan(value, text); + return parsedEnd == text ? TFloat{0} : value; } } @@ -124,6 +233,32 @@ void* APS5_VABI memrchr_nid_postfix(const void* s, int c, size_t n) { return nullptr; } +void* APS5_VABI memccpy_nid_postfix(void* dest, const void* src, int c, size_t n) { + auto* d = static_cast(dest); + const auto* s = static_cast(src); + const auto target = static_cast(c); + for (size_t i = 0; i < n; ++i) { + d[i] = s[i]; + if (s[i] == target) return d + i + 1; + } + return nullptr; +} + +void* APS5_VABI memmem_nid_postfix(const void* haystack, size_t haystacklen, const void* needle, size_t needlelen) { + if (needlelen == 0) return const_cast(haystack); + if (haystacklen < needlelen) return nullptr; + const auto* h = static_cast(haystack); + const auto* n = static_cast(needle); + const unsigned char first = n[0]; + const size_t limit = haystacklen - needlelen; + for (size_t i = 0; i <= limit; ++i) { + if (h[i] == first && std::memcmp(h + i, n, needlelen) == 0) { + return const_cast(h + i); + } + } + return nullptr; +} + int APS5_VABI strcmp_nid_postfix(const char* s1, const char* s2) { return std::strcmp(s1, s2); } @@ -152,8 +287,12 @@ const char* APS5_VABI strchr_nid_postfix(const char* s, int c) { return std::strchr(s, c); } +char* APS5_VABI index_nid_postfix(const char* s, int c) { + return const_cast(strchr_nid_postfix(s, 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) { @@ -180,6 +319,7 @@ size_t APS5_VABI strlcpy_nid_postfix(char* dest, const char* src, size_t size) { bool StopAtPrefixLetter_nid_no_patch(const char* str, char** endptr, int base) { if (base != 0 && base != 2 && base != 16) return false; + const char* const start = str; while (*str == ' ' || (*str >= '\t' && *str <= '\r')) ++str; if (*str == '+' || *str == '-') ++str; if (str[0] != '0' || str[1] == '\0') return false; @@ -188,27 +328,34 @@ bool StopAtPrefixLetter_nid_no_patch(const char* str, char** endptr, int base) { const bool hexDigit = (next >= '0' && next <= '9') || (next >= 'a' && next <= 'f') || (next >= 'A' && next <= 'F'); const bool bareHex = base != 2 && (str[1] == 'x' || str[1] == 'X') && !hexDigit; if (!binary && !bareHex) return false; - if (endptr) *endptr = const_cast(str + 1); + if (endptr) *endptr = const_cast(binary ? str + 1 : start); + return true; +} + +bool RejectIntegerBase_nid_no_patch(const char* str, char** endptr, int base, bool setError) { + if (base == 0 || (base >= 2 && base <= 36)) return false; + if (endptr) *endptr = const_cast(str); + if (setError) errno = EINVAL; return true; } std::int64_t APS5_VABI strtol_nid_postfix(const char* str, char** endptr, int base) { - if (StopAtPrefixLetter_nid_no_patch(str, endptr, base)) return 0; + if (RejectIntegerBase_nid_no_patch(str, endptr, base, true) || StopAtPrefixLetter_nid_no_patch(str, endptr, base)) return 0; return ParseInteger(str, endptr, base, std::strtoll); } std::uint64_t APS5_VABI strtoul_nid_postfix(const char* str, char** endptr, int base) { - if (StopAtPrefixLetter_nid_no_patch(str, endptr, base)) return 0; + if (RejectIntegerBase_nid_no_patch(str, endptr, base, true) || StopAtPrefixLetter_nid_no_patch(str, endptr, base)) return 0; return ParseInteger(str, endptr, base, std::strtoull); } long long APS5_VABI strtoll_nid_postfix(const char* str, char** endptr, int base) { - if (StopAtPrefixLetter_nid_no_patch(str, endptr, base)) return 0; + if (RejectIntegerBase_nid_no_patch(str, endptr, base, true) || StopAtPrefixLetter_nid_no_patch(str, endptr, base)) return 0; return ParseInteger(str, endptr, base, std::strtoll); } unsigned long long APS5_VABI strtoull_nid_postfix(const char* str, char** endptr, int base) { - if (StopAtPrefixLetter_nid_no_patch(str, endptr, base)) return 0; + if (RejectIntegerBase_nid_no_patch(str, endptr, base, true) || StopAtPrefixLetter_nid_no_patch(str, endptr, base)) return 0; return ParseInteger(str, endptr, base, std::strtoull); } @@ -230,15 +377,15 @@ long double APS5_VABI strtold_nid_postfix(const char* str, char** endptr) { } int APS5_VABI atoi_nid_postfix(const char* str) { - return static_cast(strtol_nid_postfix(str, nullptr, 10)); + return static_cast(strtoull_nid_postfix(str, nullptr, 10)); } std::int64_t APS5_VABI atol_nid_postfix(const char* str) { - return ParseInteger(str, nullptr, 10, std::strtoll); + return static_cast(strtoull_nid_postfix(str, nullptr, 10)); } long long APS5_VABI atoll_nid_postfix(const char* str) { - return ParseInteger(str, nullptr, 10, std::strtoll); + return static_cast(strtoull_nid_postfix(str, nullptr, 10)); } std::div_t APS5_VABI div_nid_postfix(int numerator, int denominator) { @@ -412,7 +559,7 @@ char* APS5_VABI strnstr_nid_postfix(const char* haystack, const char* needle, si } unsigned long long APS5_VABI _Stoull_nid_postfix(const char* str, char** endptr, int base) { - if (StopAtPrefixLetter_nid_no_patch(str, endptr, base)) return 0; + if (RejectIntegerBase_nid_no_patch(str, endptr, base, false) || StopAtPrefixLetter_nid_no_patch(str, endptr, base)) return 0; return ParseInteger(str, endptr, base, std::strtoull); } @@ -542,20 +689,40 @@ long double APS5_VABI wcstold_nid_postfix(const char16_t* str, char16_t** endptr return ParseAsciiPrefix(str, endptr, [](const char* text, char** end) { return ParseFloat(text, end, std::strtold); }); } -long long APS5_VABI wcstol_nid_postfix(const char16_t* str, char16_t** endptr, int base) { - return ParseAsciiPrefix(str, endptr, [base](const char* text, char** end) { return StopAtPrefixLetter_nid_no_patch(text, end, base) ? 0LL : ParseInteger(text, end, base, std::strtoll); }); -} - -long long APS5_VABI wcstoll_nid_postfix(const char16_t* str, char16_t** endptr, int base) { - return ParseAsciiPrefix(str, endptr, [base](const char* text, char** end) { return StopAtPrefixLetter_nid_no_patch(text, end, base) ? 0LL : ParseInteger(text, end, base, std::strtoll); }); -} - unsigned long long APS5_VABI wcstoul_nid_postfix(const char16_t* str, char16_t** endptr, int base) { - return ParseAsciiPrefix(str, endptr, [base](const char* text, char** end) { return StopAtPrefixLetter_nid_no_patch(text, end, base) ? 0ULL : ParseInteger(text, end, base, std::strtoull); }); + return ParseAsciiPrefix(str, endptr, [base](const char* text, char** end) { return RejectIntegerBase_nid_no_patch(text, end, base, false) || StopAtPrefixLetter_nid_no_patch(text, end, base) ? 0ULL : ParseInteger(text, end, base, std::strtoull); }); } unsigned long long APS5_VABI wcstoull_nid_postfix(const char16_t* str, char16_t** endptr, int base) { - return ParseAsciiPrefix(str, endptr, [base](const char* text, char** end) { return StopAtPrefixLetter_nid_no_patch(text, end, base) ? 0ULL : ParseInteger(text, end, base, std::strtoull); }); + return ParseAsciiPrefix(str, endptr, [base](const char* text, char** end) { return RejectIntegerBase_nid_no_patch(text, end, base, false) || StopAtPrefixLetter_nid_no_patch(text, end, base) ? 0ULL : ParseInteger(text, end, base, std::strtoull); }); +} + +long long APS5_VABI wcstoll_nid_postfix(const char16_t* str, char16_t** endptr, int base) { + const char16_t* digits = str; + while (*digits == u' ' || (*digits >= u'\t' && *digits <= u'\r')) ++digits; + const bool negative = *digits == u'-'; + if (*digits == u'+' || *digits == u'-') ++digits; + const int saved = errno; + errno = 0; + char16_t* stop = nullptr; + const unsigned long long magnitude = wcstoull_nid_postfix(digits, &stop, base); + const bool overflowed = errno == ERANGE; + errno = saved; + if (stop == digits) { + if (endptr) *endptr = const_cast(str); + return 0; + } + if (endptr) *endptr = stop; + const unsigned long long limit = negative ? 0x8000000000000000ULL : 0x7fffffffffffffffULL; + if (overflowed || magnitude > limit) { + errno = ERANGE; + return negative ? std::numeric_limits::min() : std::numeric_limits::max(); + } + return static_cast(negative ? 0 - magnitude : magnitude); +} + +long long APS5_VABI wcstol_nid_postfix(const char16_t* str, char16_t** endptr, int base) { + return wcstoll_nid_postfix(str, endptr, base); } long long APS5_VABI wcstoimax_nid_postfix(const char16_t* str, char16_t** endptr, int base) { diff --git a/core/libs/prx/libc/src/Threads.cpp b/core/libs/prx/libc/src/Threads.cpp index 167360e72c..3373def131 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 } } @@ -304,5 +384,118 @@ int APS5_VABI _Cnd_signal_nid_postfix(void** handle) { return SignalCondition_nid_no_patch(handle); } +struct GuestPad { + unsigned (APS5_VABI* const* vtable)(GuestPad*); + void* condition; + void* mutex; + bool started; +}; + +static_assert(offsetof(GuestPad, condition) == 0x8 && offsetof(GuestPad, mutex) == 0x10 && offsetof(GuestPad, started) == 0x18 && sizeof(GuestPad) == 0x20); + +[[noreturn]] void ThrowCppError_nid_no_patch(int code); + +static void RequireThrd(int result, const char* what) { + switch (result) { + case threadSuccess: + return; + case threadNomem: + case threadTimedout: + ThrowCppError_nid_no_patch(6); + case threadBusy: + ThrowCppError_nid_no_patch(0); + case threadError: + ThrowCppError_nid_no_patch(1); + } + throw std::runtime_error(std::string(what) + " failed with " + std::to_string(result)); +} + +static std::atomic registeredPthreadCreate{nullptr}; +static std::atomic registeredPthreadJoin{nullptr}; +static std::atomic registeredPthreadDetach{nullptr}; + +void RegisterPthreadCreate_nid_no_patch(PthreadCreateFn_nid_no_patch fn) { + registeredPthreadCreate.store(fn, std::memory_order_release); +} + +void RegisterPthreadJoin_nid_no_patch(PthreadJoinFn_nid_no_patch fn) { + registeredPthreadJoin.store(fn, std::memory_order_release); +} + +void RegisterPthreadDetach_nid_no_patch(PthreadDetachFn_nid_no_patch fn) { + registeredPthreadDetach.store(fn, std::memory_order_release); +} + +static void* APS5_VABI PadEntry_nid_no_patch(void* argument) { + auto* pad = static_cast(argument); + const auto go = pad->vtable[0]; + return reinterpret_cast(static_cast(go(pad))); +} + +void APS5_VABI _ZNSt4_PadC2Ev_nid_postfix(GuestPad* pad) { + if (!pad) APS5_INVALID_ARG_EX; + RequireThrd(_Cnd_init_nid_postfix(&pad->condition), "_Pad::_Pad: _Cnd_init"); + const int created = _Mtx_init_nid_postfix(&pad->mutex, 1); + if (created != threadSuccess) { + _Cnd_destroy_nid_postfix(&pad->condition); + RequireThrd(created, "_Pad::_Pad: _Mtx_init"); + } + pad->started = false; + const int locked = _Mtx_lock_nid_postfix(&pad->mutex); + if (locked != threadSuccess) { + _Mtx_destroy_nid_postfix(&pad->mutex); + _Cnd_destroy_nid_postfix(&pad->condition); + RequireThrd(locked, "_Pad::_Pad: _Mtx_lock"); + } +} + +void APS5_VABI _ZNSt4_PadD2Ev_nid_postfix(GuestPad* pad) { + if (!pad) APS5_INVALID_ARG_EX; + RequireThrd(_Mtx_unlock_nid_postfix(&pad->mutex), "_Pad::~_Pad: _Mtx_unlock"); + _Mtx_destroy_nid_postfix(&pad->mutex); + _Cnd_destroy_nid_postfix(&pad->condition); +} + +void APS5_VABI _ZNSt4_Pad8_ReleaseEv_nid_postfix(GuestPad* pad) { + if (!pad) APS5_INVALID_ARG_EX; + RequireThrd(_Mtx_lock_nid_postfix(&pad->mutex), "_Pad::_Release: _Mtx_lock"); + pad->started = true; + RequireThrd(_Cnd_signal_nid_postfix(&pad->condition), "_Pad::_Release: _Cnd_signal"); + RequireThrd(_Mtx_unlock_nid_postfix(&pad->mutex), "_Pad::_Release: _Mtx_unlock"); +} + +void APS5_VABI _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(GuestPad* pad, void** thread) { + if (!pad || !thread) APS5_INVALID_ARG_EX; + const auto create = registeredPthreadCreate.load(std::memory_order_acquire); + if (!create) throw std::runtime_error("_Pad::_Launch: pthread_create is not registered"); + const int created = create(thread, nullptr, PadEntry_nid_no_patch, pad); + if (created != 0) ThrowCppError_nid_no_patch(6); + while (!pad->started) RequireThrd(_Cnd_wait_nid_postfix(&pad->condition, &pad->mutex), "_Pad::_Launch: _Cnd_wait"); +} + +int APS5_VABI _Thrd_join_nid_postfix(void* thread, int* code) { + const auto join = registeredPthreadJoin.load(std::memory_order_acquire); + if (!join) throw std::runtime_error("_Thrd_join: pthread_join is not registered"); + void* result = nullptr; + if (join(thread, &result) != 0) return threadError; + if (code) *code = static_cast(reinterpret_cast(result)); + return threadSuccess; +} + +int APS5_VABI _Thrd_detach_nid_postfix(void* thread) { + const auto detach = registeredPthreadDetach.load(std::memory_order_acquire); + if (!detach) throw std::runtime_error("_Thrd_detach: pthread_detach is not registered"); + return detach(thread) == 0 ? threadSuccess : threadError; +} + +int APS5_VABI _Thrd_id_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +[[noreturn]] void APS5_VABI _ZSt16_Throw_Cpp_errori_nid_postfix(int code) { + ThrowCppError_nid_no_patch(code); +} + } diff --git a/core/libs/prx/libc/src/Time.cpp b/core/libs/prx/libc/src/Time.cpp index 5b4e24c395..c3499f8047 100644 --- a/core/libs/prx/libc/src/Time.cpp +++ b/core/libs/prx/libc/src/Time.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -18,6 +19,8 @@ #include "prx/libc/include/GuestTime.hpp" #include "prx/libc/include/SonyCrt.hpp" +extern "C" char* tzname_nid_postfix[2]; + namespace { constexpr const char* WEEKDAY_NAMES[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; @@ -158,6 +161,80 @@ extern "C" int64_t APS5_VABI libc_mktime_nid_postfix(GuestTm* timeptr); namespace { +bool setHostZone(const char* zone) { +#ifdef _WIN32 + return _putenv_s("TZ", zone == nullptr ? "" : zone) == 0; +#else + return zone == nullptr ? ::unsetenv("TZ") == 0 : ::setenv("TZ", zone, 1) == 0; +#endif +} + +const char* hostZoneFor(const char* guest) { +#ifdef _WIN32 + return guest != nullptr && guest[0] == 0 ? "UTC0" : guest; +#else + return guest == nullptr ? ":/etc/localtime" : guest; +#endif +} + +std::recursive_mutex& zoneMutex() { + static std::recursive_mutex mutex; + return mutex; +} + +char** HostZoneNames(); + +void applyGuestZone() { + static bool initialized = false; + static bool present = false; + static std::string current; + if (!initialized) { + const char* host = std::getenv("TZ"); + present = host != nullptr; + if (present) current = host; + initialized = true; + } + std::string guest; + const bool guestPresent = CopyGuestEnvironment_nid_no_patch("TZ", guest); + if (guestPresent == present && (!guestPresent || current == guest)) return; + if (!setHostZone(hostZoneFor(guestPresent ? guest.c_str() : nullptr))) throw std::runtime_error("tzset: the host rejected the guest TZ"); + present = guestPresent; + current = guest; +#ifdef _WIN32 + _tzset(); +#else + ::tzset(); +#endif + char** names = HostZoneNames(); + tzname_nid_postfix[0] = names[0]; + tzname_nid_postfix[1] = names[1]; +} + +int& zoneDepth() { + static thread_local int depth = 0; + return depth; +} + +class ZoneScope { +public: + ZoneScope() : lock(zoneMutex()) { + if (zoneDepth() == 0) applyGuestZone(); + ++zoneDepth(); + } + + ~ZoneScope() { --zoneDepth(); } + + ZoneScope(const ZoneScope&) = delete; + ZoneScope& operator=(const ZoneScope&) = delete; + +private: + std::lock_guard lock; +}; + +} // namespace + +namespace { + constexpr const char* WEEKDAY_FULL_NAMES[] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}; constexpr const char* MONTH_FULL_NAMES[] = {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}; @@ -443,17 +520,29 @@ char** HostZoneNames() { extern "C" { char* tzname_nid_postfix[2] = {HostZoneNames()[0], HostZoneNames()[1]}; +int64_t timezone_nid_postfix = 0; +int daylight_nid_postfix = 0; GuestTm* APS5_VABI localtime_s_nid_postfix(const int64_t* timer, GuestTm* result); GuestTm* APS5_VABI gmtime_s_nid_postfix(const int64_t* timer, GuestTm* result); +int64_t APS5_VABI libc_time_nid_postfix(int64_t* timer); +double APS5_VABI libc_difftime_nid_postfix(int64_t time1, int64_t time0); std::size_t APS5_VABI wcsrtombs_nid_postfix(char* destination, const std::uint16_t** source, std::size_t capacity, void* state); std::size_t APS5_VABI mbsrtowcs_nid_postfix(std::uint16_t* destination, const char** source, std::size_t capacity, void* state); int APS5_VABI ___mb_cur_max_nid_postfix(); void APS5_VABI tzset_nid_postfix(void) { + const ZoneScope zone; char** names = HostZoneNames(); tzname_nid_postfix[0] = names[0]; tzname_nid_postfix[1] = names[1]; + const int64_t now = libc_time_nid_postfix(nullptr); + GuestTm universal{}; + GuestTm local{}; + gmtime_s_nid_postfix(&now, &universal); + localtime_s_nid_postfix(&now, &local); + daylight_nid_postfix = local.tm_isdst >= 0 ? 1 : 0; + timezone_nid_postfix = static_cast(libc_difftime_nid_postfix(libc_mktime_nid_postfix(&universal), libc_mktime_nid_postfix(&local))); } int64_t APS5_VABI libc_time_nid_postfix(int64_t* timer) { @@ -504,11 +593,13 @@ GuestTm* APS5_VABI localtime_nid_postfix(const int64_t* timer) { GuestTm* APS5_VABI localtime_s_nid_postfix(const int64_t* timer, GuestTm* result) { if (timer == nullptr || result == nullptr) return nullptr; + const ZoneScope zone; std::tm host{}; 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; @@ -553,6 +644,7 @@ GuestTm* APS5_VABI gmtime_nid_postfix(const int64_t* timer) { } int64_t APS5_VABI libc_mktime_nid_postfix(GuestTm* timeptr) { + const ZoneScope zone; std::tm host = toHostTm(*timeptr); host.tm_wday = -1; int64_t moved = 0; @@ -594,6 +686,7 @@ int64_t APS5_VABI mktime_nid_postfix(GuestTm* timeptr) { } size_t APS5_VABI libc_strftime_nid_postfix(char* str, size_t count, const char* format, const GuestTm* timeptr) { + const ZoneScope zone; tzset_nid_postfix(); return formatGuestTime(str, count, format, *timeptr); } diff --git a/core/libs/prx/libc/src/TimeExtras.cpp b/core/libs/prx/libc/src/TimeExtras.cpp new file mode 100644 index 0000000000..ee2a225b25 --- /dev/null +++ b/core/libs/prx/libc/src/TimeExtras.cpp @@ -0,0 +1,93 @@ +#include + +#include "prx/libc/include/General.hpp" +#include "prx/libc/include/GuestTime.hpp" + +namespace { + +struct Xtime { + std::int64_t sec; + std::int64_t nsec; +}; + +constexpr int timeUtc = 1; +constexpr std::int64_t nanosPerSecond = 1000000000; +constexpr std::int64_t secondsPerDay = 86400; + +std::int64_t floorDivide(std::int64_t value, std::int64_t divisor) { + return value >= 0 ? value / divisor : -((-(value + 1)) / divisor + 1); +} + +std::int64_t daysFromCivil(std::int64_t year, std::int64_t month, std::int64_t day) { + year -= month <= 2 ? 1 : 0; + const std::int64_t era = floorDivide(year, 400); + const std::int64_t yearOfEra = year - era * 400; + const std::int64_t dayOfYear = (153 * (month > 2 ? month - 3 : month + 9) + 2) / 5 + day - 1; + const std::int64_t dayOfEra = yearOfEra * 365 + yearOfEra / 4 - yearOfEra / 100 + dayOfYear; + return era * 146097 + dayOfEra - 719468; +} + +std::int64_t wrappingAdd(std::int64_t left, std::int64_t right) { + return static_cast(static_cast(left) + static_cast(right)); +} + +} // namespace + +extern "C" { + +GuestTm* APS5_VABI gmtime_s_nid_postfix(const std::int64_t* timer, GuestTm* result); +GuestTm* APS5_VABI localtime_s_nid_postfix(const std::int64_t* timer, GuestTm* result); +std::int64_t APS5_VABI _Xtime_get_ticks_nid_postfix(); + +GuestTm* APS5_VABI gmtime_r_nid_postfix(const std::int64_t* timer, GuestTm* result) { + return gmtime_s_nid_postfix(timer, result); +} + +GuestTm* APS5_VABI localtime_r_nid_postfix(const std::int64_t* timer, GuestTm* result) { + return localtime_s_nid_postfix(timer, result); +} + +std::int64_t APS5_VABI timegm_nid_postfix(GuestTm* time) { + const std::int64_t years = floorDivide(time->tm_mon, 12); + const std::int64_t month = time->tm_mon - years * 12 + 1; + const std::int64_t year = std::int64_t{time->tm_year} + 1900 + years; + const std::int64_t days = daysFromCivil(year, month, 1) + time->tm_mday - 1; + const std::int64_t seconds = days * secondsPerDay + std::int64_t{time->tm_hour} * 3600 + std::int64_t{time->tm_min} * 60 + time->tm_sec; + GuestTm normalized{}; + if (gmtime_s_nid_postfix(&seconds, &normalized) == nullptr) return -1; + *time = normalized; + return seconds; +} + +int APS5_VABI xtime_get_nid_postfix(Xtime* time, int base) { + if (time == nullptr || base != timeUtc) return 0; + const std::int64_t micros = _Xtime_get_ticks_nid_postfix(); + time->sec = floorDivide(micros, 1000000); + time->nsec = (micros - time->sec * 1000000) * 1000; + return timeUtc; +} + +Xtime APS5_VABI _Xtime_to_ts_nid_postfix(const Xtime* time) { + return *time; +} + +Xtime APS5_VABI _Xtime_diff_to_ts_nid_postfix(const Xtime* target) { + Xtime now{}; + xtime_get_nid_postfix(&now, timeUtc); + std::int64_t seconds = wrappingAdd(target->sec, target->nsec / nanosPerSecond); + std::int64_t nanos = target->nsec % nanosPerSecond; + if (nanos < 0) { + nanos += nanosPerSecond; + seconds = wrappingAdd(seconds, -1); + } + seconds = wrappingAdd(seconds, -now.sec); + if (nanos < now.nsec) { + nanos += nanosPerSecond; + seconds = wrappingAdd(seconds, -1); + } + nanos -= now.nsec; + if (seconds < 0 || (seconds == 0 && nanos == 0)) return {0, 0}; + return {seconds, nanos}; +} + +} diff --git a/core/libs/prx/libc/src/exception/StandardExceptions.cpp b/core/libs/prx/libc/src/exception/StandardExceptions.cpp index 399f9e6a69..31feeb3d8e 100644 --- a/core/libs/prx/libc/src/exception/StandardExceptions.cpp +++ b/core/libs/prx/libc/src/exception/StandardExceptions.cpp @@ -93,6 +93,7 @@ const char* PlainWhat(const ExceptionObject* object) { if (std::strcmp(name, "St17bad_function_call") == 0) return "std::bad_function_call"; if (std::strcmp(name, "St12bad_weak_ptr") == 0) return "bad_weak_ptr"; if (std::strcmp(name, "St18bad_variant_access") == 0) return "bad_variant_access"; + if (std::strcmp(name, "St19bad_optional_access") == 0) return "bad_optional_access"; return name; } const char* MessageWhat(const ExceptionObject* object) { return object->message ? object->message : ""; } @@ -337,6 +338,15 @@ const char* APS5_VABI _ZNKSt12bad_weak_ptr4whatEv_nid_postfix(const LibcExceptio LibcException::TypeRecord _ZTISt18bad_variant_access_nid_postfix {LibcException::SingleTypeVtable + 2, "St18bad_variant_access", &_ZTISt9exception_nid_postfix}; LibcException::ExceptionVtable _ZTVSt18bad_variant_access_nid_postfix {0, &_ZTISt18bad_variant_access_nid_postfix, LibcException::GuestDestroyPlain_nid_no_patch, LibcException::GuestDeletePlain_nid_no_patch, LibcException::GuestPlainWhat_nid_no_patch}; +LibcException::TypeRecord _ZTISt19bad_optional_access_nid_postfix {LibcException::SingleTypeVtable + 2, "St19bad_optional_access", &_ZTISt9exception_nid_postfix}; +LibcException::ExceptionVtable _ZTVSt19bad_optional_access_nid_postfix {0, &_ZTISt19bad_optional_access_nid_postfix, LibcException::GuestDestroyPlain_nid_no_patch, LibcException::GuestDeletePlain_nid_no_patch, LibcException::GuestPlainWhat_nid_no_patch}; +void APS5_VABI _ZNSt19bad_optional_accessD1Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DestroyPlain(self); } +void APS5_VABI _ZNSt19bad_optional_accessD2Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DestroyPlain(self); } +void APS5_VABI _ZNSt19bad_optional_accessD0Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DeletePlain(self); } +const char* APS5_VABI _ZNKSt19bad_optional_access4whatEv_nid_postfix(const LibcException::ExceptionObject* self) { return LibcException::PlainWhat(self); } +void APS5_VABI _ZNSt19bad_optional_accessC1Ev_nid_postfix(LibcException::ExceptionObject* self) { self->vtable = &_ZTVSt19bad_optional_access_nid_postfix.destroy; } +void APS5_VABI _ZNSt19bad_optional_accessC2Ev_nid_postfix(LibcException::ExceptionObject* self) { self->vtable = &_ZTVSt19bad_optional_access_nid_postfix.destroy; } + LibcException::TypeRecord _ZTISt11logic_error_nid_postfix {LibcException::SingleTypeVtable + 2, "St11logic_error", &_ZTISt9exception_nid_postfix}; LibcException::ExceptionVtable _ZTVSt11logic_error_nid_postfix {0, &_ZTISt11logic_error_nid_postfix, LibcException::GuestDestroyMessage_nid_no_patch, LibcException::GuestDeleteMessage_nid_no_patch, LibcException::GuestMessageWhat_nid_no_patch}; void APS5_VABI _ZNSt11logic_errorD1Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DestroyMessage(self); } @@ -473,6 +483,7 @@ const char* APS5_VABI _ZNKSt8ios_base7failure4whatEv_nid_postfix(const LibcExcep [[noreturn]] void APS5_VABI _ZNSt8__sce_v219_Xbad_function_callEv_nid_postfix() { LibcException::ThrowPlain(_ZTVSt17bad_function_call_nid_postfix); } [[noreturn]] void APS5_VABI _ZSt19_Throw_bad_weak_ptrv_nid_postfix() { LibcException::ThrowPlain(_ZTVSt12bad_weak_ptr_nid_postfix); } [[noreturn]] void APS5_VABI _ZSt26__throw_bad_variant_accessv_nid_postfix() { LibcException::ThrowPlain(_ZTVSt18bad_variant_access_nid_postfix); } +[[noreturn]] void APS5_VABI _ZSt26_Throw_bad_optional_accessv_nid_postfix() { LibcException::ThrowPlain(_ZTVSt19bad_optional_access_nid_postfix); } [[noreturn]] void APS5_VABI _ZSt13_Xregex_errorNSt15regex_constants10error_typeE_nid_postfix(std::regex_constants::error_type code) { struct RegexObject { LibcException::ExceptionObject base; std::regex_constants::error_type code; }; auto* object = static_cast(__cxa_allocate_exception_nid_postfix(sizeof(RegexObject))); diff --git a/core/libs/prx/libc/src/specifics/windows/CallTrace.cpp b/core/libs/prx/libc/src/specifics/windows/CallTrace.cpp deleted file mode 100644 index 7e30971457..0000000000 --- a/core/libs/prx/libc/src/specifics/windows/CallTrace.cpp +++ /dev/null @@ -1,202 +0,0 @@ -#ifdef _WIN32 -#include -#include "prx/libc/include/CallTrace.hpp" -#include "prx/libc/include/general/AsmMacros.hpp" -#include "prx/libc/include/general/VabiMacros.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -extern "C" void CallTraceEnter_nid_no_patch(); - -namespace { - -struct ImportRecord { - void* target; - std::string name; - std::string library; - mutable std::atomic calls{0}; -}; - -struct RingSlot { - std::atomic sequence{0}; - std::atomic thread{0}; - std::atomic returnAddress{0}; - std::atomic record{nullptr}; -}; - -constexpr std::size_t RingCapacity = 65536; -constexpr std::size_t ThunkBytes = 24; -constexpr std::size_t ThunkAreaBytes = 1024 * 1024; - -std::mutex g_mutex; -std::deque g_records; -std::unordered_map g_thunkByTarget; -std::unique_ptr g_ring; -std::atomic g_sequence{0}; -std::uint8_t* g_writable = nullptr; -std::uint8_t* g_executable = nullptr; -std::size_t g_thunks = 0; - -bool Selected(std::string_view library) { - const char* value = std::getenv("APS5_TRACE_CALLS"); - if (value == nullptr || *value == '\0') return false; - std::string_view rest(value); - if (library.ends_with(".prx")) library.remove_suffix(4); - while (!rest.empty()) { - const auto comma = rest.find(','); - auto token = rest.substr(0, comma); - rest = comma == std::string_view::npos ? std::string_view{} : rest.substr(comma + 1); - if (token.ends_with(".prx")) token.remove_suffix(4); - if (token == "all" || token == library) return true; - } - return false; -} - -void MapThunkArea() { - const HANDLE section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE, 0, static_cast(ThunkAreaBytes), nullptr); - if (section == nullptr) throw std::system_error(static_cast(GetLastError()), std::system_category(), "call trace: CreateFileMappingW"); - g_writable = static_cast(MapViewOfFile(section, FILE_MAP_WRITE, 0, 0, ThunkAreaBytes)); - g_executable = static_cast(MapViewOfFile(section, FILE_MAP_READ | FILE_MAP_EXECUTE, 0, 0, ThunkAreaBytes)); - const DWORD code = GetLastError(); - CloseHandle(section); - if (g_writable == nullptr || g_executable == nullptr) throw std::system_error(static_cast(code), std::system_category(), "call trace: MapViewOfFile"); -} - -void* WriteThunk(const ImportRecord& record) { - if ((g_thunks + 1) * ThunkBytes > ThunkAreaBytes) throw std::runtime_error("call trace: thunk area exhausted"); - const std::size_t offset = g_thunks * ThunkBytes; - const std::uint8_t prefix[] = {0x49, 0xbb}; - const std::uint8_t jump[] = {0xff, 0x25, 0, 0, 0, 0}; - const auto recordAddress = reinterpret_cast(&record); - const auto handler = reinterpret_cast(&CallTraceEnter_nid_no_patch); - std::uint8_t* thunk = g_writable + offset; - std::memcpy(thunk, prefix, sizeof(prefix)); - std::memcpy(thunk + 2, &recordAddress, sizeof(recordAddress)); - std::memcpy(thunk + 10, jump, sizeof(jump)); - std::memcpy(thunk + 16, &handler, sizeof(handler)); - FlushInstructionCache(GetCurrentProcess(), g_executable + offset, ThunkBytes); - ++g_thunks; - return g_executable + offset; -} - -} - -static_assert(offsetof(ImportRecord, target) == 0); - -extern "C" { - -void APS5_VABI CallTraceRecord_nid_no_patch(const void* record, std::uint64_t returnAddress) { - static_cast(record)->calls.fetch_add(1, std::memory_order_relaxed); - const std::uint64_t sequence = g_sequence.fetch_add(1, std::memory_order_relaxed) + 1; - RingSlot& slot = g_ring[sequence % RingCapacity]; - slot.sequence.store(0, std::memory_order_release); - slot.thread.store(GetCurrentThreadId(), std::memory_order_relaxed); - slot.returnAddress.store(returnAddress, std::memory_order_relaxed); - slot.record.store(static_cast(record), std::memory_order_relaxed); - slot.sequence.store(sequence, std::memory_order_release); -} - -void* TraceImport_nid_no_patch(void* target, const char* name, const char* library) { - if (target == nullptr || name == nullptr || library == nullptr || !Selected(library)) return target; - std::lock_guard lock(g_mutex); - if (g_ring == nullptr) { - MapThunkArea(); - g_ring = std::make_unique(RingCapacity); - } - if (const auto existing = g_thunkByTarget.find(target); existing != g_thunkByTarget.end()) return existing->second; - g_records.emplace_back(target, name, library); - void* thunk = WriteThunk(g_records.back()); - g_thunkByTarget.emplace(target, thunk); - return thunk; -} - -std::size_t CopyCallTrace_nid_no_patch(CallTraceEntry* entries, std::size_t capacity) { - if (entries == nullptr || capacity == 0 || g_ring == nullptr) return 0; - const std::uint64_t last = g_sequence.load(std::memory_order_acquire); - const std::uint64_t span = std::min({last, capacity, RingCapacity}); - std::size_t count = 0; - for (std::uint64_t sequence = last - span + 1; sequence <= last; ++sequence) { - const RingSlot& slot = g_ring[sequence % RingCapacity]; - if (slot.sequence.load(std::memory_order_acquire) != sequence) continue; - const auto* record = slot.record.load(std::memory_order_relaxed); - const CallTraceEntry entry{sequence, slot.thread.load(std::memory_order_relaxed), slot.returnAddress.load(std::memory_order_relaxed), record->name.c_str(), record->library.c_str()}; - if (slot.sequence.load(std::memory_order_acquire) != sequence) continue; - entries[count++] = entry; - } - return count; -} - -std::size_t CopyCallCounts_nid_no_patch(CallTraceCount* entries, std::size_t capacity, std::uint64_t* total) { - if (total != nullptr) *total = g_sequence.load(std::memory_order_acquire); - if (entries == nullptr || capacity == 0) return 0; - std::unique_lock lock(g_mutex, std::try_to_lock); - if (!lock.owns_lock()) return 0; - std::size_t count = 0; - for (const auto& record : g_records) { - const std::uint64_t calls = record.calls.load(std::memory_order_relaxed); - if (calls == 0 || (count == capacity && calls <= entries[count - 1].calls)) continue; - std::size_t position = count < capacity ? count++ : count - 1; - while (position > 0 && entries[position - 1].calls < calls) { - entries[position] = entries[position - 1]; - --position; - } - entries[position] = {calls, record.name.c_str(), record.library.c_str()}; - } - return count; -} - -} - -asm(".text\n" - APS5_ASM_FUNCTION("CallTraceEnter_nid_no_patch") - " push %rdi\n" - " push %rsi\n" - " push %rdx\n" - " push %rcx\n" - " push %r8\n" - " push %r9\n" - " push %rax\n" - " push %r10\n" - " push %r11\n" - " sub $128, %rsp\n" - " movdqu %xmm0, 0(%rsp)\n" - " movdqu %xmm1, 16(%rsp)\n" - " movdqu %xmm2, 32(%rsp)\n" - " movdqu %xmm3, 48(%rsp)\n" - " movdqu %xmm4, 64(%rsp)\n" - " movdqu %xmm5, 80(%rsp)\n" - " movdqu %xmm6, 96(%rsp)\n" - " movdqu %xmm7, 112(%rsp)\n" - " mov %r11, %rdi\n" - " mov 200(%rsp), %rsi\n" - " call CallTraceRecord_nid_no_patch\n" - " movdqu 0(%rsp), %xmm0\n" - " movdqu 16(%rsp), %xmm1\n" - " movdqu 32(%rsp), %xmm2\n" - " movdqu 48(%rsp), %xmm3\n" - " movdqu 64(%rsp), %xmm4\n" - " movdqu 80(%rsp), %xmm5\n" - " movdqu 96(%rsp), %xmm6\n" - " movdqu 112(%rsp), %xmm7\n" - " add $128, %rsp\n" - " pop %r11\n" - " pop %r10\n" - " pop %rax\n" - " pop %r9\n" - " pop %r8\n" - " pop %rcx\n" - " pop %rdx\n" - " pop %rsi\n" - " pop %rdi\n" - " jmp *(%r11)\n"); -#endif diff --git a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp index 3d685d640a..7b4cc729ed 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); @@ -479,6 +481,16 @@ void ReportCallTrace() { Report(" #%llu thread %llu %s %s from %s\n", static_cast(entries[index].sequence), static_cast(entries[index].thread), entries[index].library, entries[index].name, line); } + static CallTraceEntry own[32]; + const std::uint64_t thread = GetCurrentThreadId(); + const std::size_t owned = CopyThreadCallTrace_nid_no_patch(thread, own, std::size(own)); + if (owned != 0) { + Report(" last traced calls of thread %llu:\n", static_cast(thread)); + for (std::size_t index = 0; index < owned; ++index) { + DescribeAddress(own[index].returnAddress, line, sizeof(line)); + Report(" #%llu %s %s from %s\n", static_cast(own[index].sequence), own[index].library, own[index].name, line); + } + } static CallTraceCount counts[32]; std::uint64_t total = 0; const std::size_t counted = CopyCallCounts_nid_no_patch(counts, std::size(counts), &total); @@ -503,7 +515,17 @@ std::size_t CaptureFrames(const CONTEXT* context, std::uint64_t* frames, std::si return count; } -void ReportThreadContext(const CONTEXT* context, DWORD threadId, const char* name, const std::uint64_t* frames, std::size_t frameCount) { +std::size_t CaptureStackCandidates(std::uint64_t rsp, std::uint64_t* candidates, std::size_t capacity) { + std::size_t count = 0; + for (std::uint64_t slot = rsp; count < capacity && slot < rsp + 0x8000; slot += 8) { + if (!IsReadable(slot)) break; + const auto value = *reinterpret_cast(slot); + if (IsExecutable(value)) candidates[count++] = value; + } + return count; +} + +void ReportThreadContext(const CONTEXT* context, DWORD threadId, const char* name, const std::uint64_t* frames, std::size_t frameCount, const std::uint64_t* candidates, std::size_t candidateCount) { char line[MAX_PATH + 64]; Report(" thread %lu '%s': rip ", static_cast(threadId), name ? name : ""); DescribeAddress(context->Rip, line, sizeof(line)); @@ -512,6 +534,11 @@ void ReportThreadContext(const CONTEXT* context, DWORD threadId, const char* nam DescribeAddress(frames[depth], line, sizeof(line)); Report(" #%llu %s\n", static_cast(depth), line); } + if (candidateCount != 0) Report(" stack return address candidates:\n"); + for (std::size_t index = 0; index < candidateCount; ++index) { + DescribeAddress(candidates[index], line, sizeof(line)); + Report(" %s\n", line); + } } void ReportAllThreads() { @@ -527,6 +554,8 @@ void ReportAllThreads() { CONTEXT context; std::uint64_t frames[12]; std::size_t frameCount; + std::uint64_t candidates[24]; + std::size_t candidateCount; }; static CapturedThread captured[128]; std::size_t capturedCount = 0; @@ -549,6 +578,8 @@ void ReportAllThreads() { const BOOL haveContext = GetThreadContext(thread, &context); std::uint64_t frames[12] = {}; const std::size_t frameCount = haveContext ? CaptureFrames(&context, frames, sizeof(frames) / sizeof(frames[0])) : 0; + std::uint64_t candidates[24] = {}; + const std::size_t candidateCount = haveContext && frameCount < 2 ? CaptureStackCandidates(context.Rsp, candidates, sizeof(candidates) / sizeof(candidates[0])) : 0; ResumeThread(thread); char name[128] = ""; PWSTR description = nullptr; @@ -563,13 +594,15 @@ void ReportAllThreads() { captured[capturedCount].context = context; std::memcpy(captured[capturedCount].frames, frames, sizeof(captured[capturedCount].frames)); captured[capturedCount].frameCount = frameCount; + std::memcpy(captured[capturedCount].candidates, candidates, sizeof(captured[capturedCount].candidates)); + captured[capturedCount].candidateCount = candidateCount; ++capturedCount; } while (Thread32Next(snapshot, &entry)); } CloseHandle(snapshot); Report(" all threads:\n"); for (std::size_t i = 0; i < capturedCount; ++i) { - ReportThreadContext(&captured[i].context, captured[i].id, captured[i].name, captured[i].frames, captured[i].frameCount); + ReportThreadContext(&captured[i].context, captured[i].id, captured[i].name, captured[i].frames, captured[i].frameCount, captured[i].candidates, captured[i].candidateCount); } if (skippedCount != 0) Report(" %llu further thread(s) left out\n", static_cast(skippedCount)); } diff --git a/core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp b/core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp new file mode 100644 index 0000000000..96d6c872a9 --- /dev/null +++ b/core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp @@ -0,0 +1,448 @@ +#if defined(_WIN32) || defined(__linux__) +#ifdef _WIN32 +#include +#else +#include +#include +#include +#include +#include +#include +#endif +#include "prx/libc/include/CallTrace.hpp" +#include "prx/libc/include/general/AsmMacros.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" void CallTraceEnter_nid_no_patch(); + +namespace { + +struct ImportRecord { + void* target = nullptr; + std::string name; + std::string library; + mutable std::atomic calls{0}; +}; + +struct RingSlot { + std::atomic sequence{0}; + std::atomic thread{0}; + std::atomic returnAddress{0}; + std::atomic record{nullptr}; +}; + +constexpr std::size_t RingCapacity = 65536; +constexpr std::size_t ThunkBytes = 24; +constexpr std::size_t ThunkAreaBytes = 1024 * 1024; +constexpr std::size_t RecordCapacity = ThunkAreaBytes / ThunkBytes; + +std::mutex g_mutex; +std::unordered_map g_thunkByTarget; +std::atomic g_records{nullptr}; +std::atomic g_recordCount{0}; +std::atomic g_ring{nullptr}; +std::atomic g_sequence{0}; +std::uint8_t* g_writable = nullptr; +std::uint8_t* g_executable = nullptr; + +bool Selected(std::string_view library) { + const char* value = std::getenv("APS5_TRACE_CALLS"); + if (value == nullptr || *value == '\0') return false; + std::string_view rest(value); + if (library.ends_with(".prx")) library.remove_suffix(4); + while (!rest.empty()) { + const auto comma = rest.find(','); + auto token = rest.substr(0, comma); + rest = comma == std::string_view::npos ? std::string_view{} : rest.substr(comma + 1); + if (token.ends_with(".prx")) token.remove_suffix(4); + if (token == "all" || token == library) return true; + } + return false; +} + +std::uint64_t CurrentThread() { +#ifdef _WIN32 + return GetCurrentThreadId(); +#else + return static_cast(gettid()); +#endif +} + +#ifdef _WIN32 +void MapThunkArea() { + const HANDLE section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE, 0, static_cast(ThunkAreaBytes), nullptr); + if (section == nullptr) throw std::system_error(static_cast(GetLastError()), std::system_category(), "call trace: CreateFileMappingW"); + g_writable = static_cast(MapViewOfFile(section, FILE_MAP_WRITE, 0, 0, ThunkAreaBytes)); + g_executable = static_cast(MapViewOfFile(section, FILE_MAP_READ | FILE_MAP_EXECUTE, 0, 0, ThunkAreaBytes)); + const DWORD code = GetLastError(); + CloseHandle(section); + if (g_writable == nullptr || g_executable == nullptr) throw std::system_error(static_cast(code), std::system_category(), "call trace: MapViewOfFile"); +} +#else +void MapThunkArea() { + const int file = memfd_create("aps5-call-trace", MFD_CLOEXEC); + if (file < 0) throw std::system_error(errno, std::generic_category(), "call trace: memfd_create"); + void* writable = MAP_FAILED; + void* executable = MAP_FAILED; + if (ftruncate(file, ThunkAreaBytes) == 0) { + writable = mmap(nullptr, ThunkAreaBytes, PROT_READ | PROT_WRITE, MAP_SHARED, file, 0); + if (writable != MAP_FAILED) executable = mmap(nullptr, ThunkAreaBytes, PROT_READ | PROT_EXEC, MAP_SHARED, file, 0); + } + const int code = errno; + close(file); + if (writable == MAP_FAILED || executable == MAP_FAILED) { + if (writable != MAP_FAILED) munmap(writable, ThunkAreaBytes); + throw std::system_error(code, std::generic_category(), "call trace: mmap"); + } + g_writable = static_cast(writable); + g_executable = static_cast(executable); +} + +struct ModuleSegment { + std::uint64_t start; + std::uint64_t end; + std::uint64_t base; + char name[64]; +}; + +struct ModuleMap { + std::size_t count; + ModuleSegment segments[256]; +}; + +struct sigaction g_previousAbort{}; +std::atomic g_modules{nullptr}; +std::atomic g_reporting{false}; +std::atomic g_reported{false}; +unsigned long long g_moduleChanges = ~0ull; + +void SnapshotModules() { + unsigned long long changes = 0; + dl_iterate_phdr([](dl_phdr_info* info, std::size_t, void* data) { + *static_cast(data) = info->dlpi_adds + info->dlpi_subs; + return 1; + }, &changes); + if (changes == g_moduleChanges) return; + g_moduleChanges = changes; + auto* map = new ModuleMap{}; + dl_iterate_phdr([](dl_phdr_info* info, std::size_t, void* data) { + auto& modules = *static_cast(data); + const char* path = info->dlpi_name != nullptr && *info->dlpi_name != '\0' ? info->dlpi_name : program_invocation_short_name; + const char* slash = std::strrchr(path, '/'); + const char* name = slash == nullptr ? path : slash + 1; + for (int index = 0; index < info->dlpi_phnum && modules.count < std::size(modules.segments); ++index) { + const auto& header = info->dlpi_phdr[index]; + if (header.p_type != PT_LOAD || header.p_memsz == 0) continue; + auto& segment = modules.segments[modules.count++]; + segment.base = info->dlpi_addr; + segment.start = info->dlpi_addr + header.p_vaddr; + segment.end = segment.start + header.p_memsz; + std::strncpy(segment.name, name, sizeof(segment.name) - 1); + } + return 0; + }, map); + g_modules.store(map, std::memory_order_release); +} + +class ReportLine { +public: + ReportLine& Text(const char* text) { + while (*text != '\0' && _length < sizeof(_buffer)) _buffer[_length++] = *text++; + return *this; + } + + ReportLine& Decimal(std::uint64_t value, std::size_t width = 0) { + char digits[20]; + std::size_t count = 0; + do { + digits[count++] = static_cast('0' + value % 10); + value /= 10; + } while (value != 0); + for (std::size_t pad = count; pad < width; ++pad) Text(" "); + while (count > 0 && _length < sizeof(_buffer)) _buffer[_length++] = digits[--count]; + return *this; + } + + ReportLine& Hex(std::uint64_t value, std::size_t digits) { + while (digits < 16 && (value >> (digits * 4)) != 0) ++digits; + Text("0x"); + for (std::size_t index = digits; index > 0 && _length < sizeof(_buffer); --index) { + _buffer[_length++] = "0123456789abcdef"[(value >> ((index - 1) * 4)) & 0xf]; + } + return *this; + } + + void Write() { + Text("\n"); + std::size_t written = 0; + while (written < _length) { + const ssize_t result = ::write(STDERR_FILENO, _buffer + written, _length - written); + if (result < 0 && errno == EINTR) continue; + if (result <= 0) break; + written += static_cast(result); + } + _length = 0; + } + +private: + char _buffer[512]; + std::size_t _length = 0; +}; + +void DescribeAddress(ReportLine& line, std::uint64_t address) { + line.Hex(address, 16); + const ModuleMap* modules = g_modules.load(std::memory_order_acquire); + if (modules == nullptr) return; + for (std::size_t index = 0; index < modules->count; ++index) { + const auto& segment = modules->segments[index]; + if (address < segment.start || address >= segment.end) continue; + line.Text(" ").Text(segment.name).Text("+"); + line.Hex(address - segment.base, 1); + return; + } +} + +void ReportCallTrace() { + static CallTraceEntry entries[64]; + static CallTraceCount counts[32]; + ReportLine line; + const std::size_t count = CopyCallTrace_nid_no_patch(entries, std::size(entries)); + if (count == 0) return; + line.Text(" last traced system library calls (APS5_TRACE_CALLS):").Write(); + for (std::size_t index = 0; index < count; ++index) { + line.Text(" #").Decimal(entries[index].sequence).Text(" thread ").Decimal(entries[index].thread); + line.Text(" ").Text(entries[index].library).Text(" ").Text(entries[index].name).Text(" from "); + DescribeAddress(line, entries[index].returnAddress); + line.Write(); + } + static CallTraceEntry own[32]; + const std::uint64_t thread = CurrentThread(); + const std::size_t owned = CopyThreadCallTrace_nid_no_patch(thread, own, std::size(own)); + if (owned != 0) { + line.Text(" last traced calls of thread ").Decimal(thread).Text(":").Write(); + for (std::size_t index = 0; index < owned; ++index) { + line.Text(" #").Decimal(own[index].sequence).Text(" ").Text(own[index].library).Text(" ").Text(own[index].name).Text(" from "); + DescribeAddress(line, own[index].returnAddress); + line.Write(); + } + } + std::uint64_t total = 0; + const std::size_t counted = CopyCallCounts_nid_no_patch(counts, std::size(counts), &total); + if (counted == 0) return; + line.Text(" most called traced imports (").Decimal(total).Text(" calls in total):").Write(); + for (std::size_t index = 0; index < counted; ++index) { + line.Text(" ").Decimal(counts[index].calls, 12).Text(" ").Text(counts[index].library).Text(" ").Text(counts[index].name).Write(); + } +} + +void ReportOnAbort(int number, siginfo_t* info, void* context) { + const int savedErrno = errno; + if (!g_reporting.exchange(true, std::memory_order_acq_rel)) { + ReportLine line; + line.Text("[libc] SIGABRT").Write(); + ReportCallTrace(); + g_reported.store(true, std::memory_order_release); + } else { + const timespec pause{0, 1000000}; + for (int waited = 0; waited < 2000 && !g_reported.load(std::memory_order_acquire); ++waited) nanosleep(&pause, nullptr); + } + errno = savedErrno; + if ((g_previousAbort.sa_flags & SA_SIGINFO) != 0) { + if (g_previousAbort.sa_sigaction != nullptr) g_previousAbort.sa_sigaction(number, info, context); + return; + } + if (g_previousAbort.sa_handler == SIG_IGN) return; + if (g_previousAbort.sa_handler != SIG_DFL) { + g_previousAbort.sa_handler(number); + return; + } + struct sigaction fallback{}; + fallback.sa_handler = SIG_DFL; + sigemptyset(&fallback.sa_mask); + sigaction(number, &fallback, nullptr); + raise(number); +} + +void InstallAbortReport() { + struct sigaction action{}; + action.sa_sigaction = ReportOnAbort; + action.sa_flags = SA_SIGINFO; + sigemptyset(&action.sa_mask); + if (sigaction(SIGABRT, &action, &g_previousAbort) != 0) throw std::system_error(errno, std::generic_category(), "call trace: sigaction"); +} +#endif + +void* WriteThunk(const ImportRecord& record, const std::size_t index) { + const std::size_t offset = index * ThunkBytes; + const std::uint8_t prefix[] = {0x49, 0xbb}; + const std::uint8_t jump[] = {0xff, 0x25, 0, 0, 0, 0}; + const auto recordAddress = reinterpret_cast(&record); + const auto handler = reinterpret_cast(&CallTraceEnter_nid_no_patch); + std::uint8_t* thunk = g_writable + offset; + std::memcpy(thunk, prefix, sizeof(prefix)); + std::memcpy(thunk + 2, &recordAddress, sizeof(recordAddress)); + std::memcpy(thunk + 10, jump, sizeof(jump)); + std::memcpy(thunk + 16, &handler, sizeof(handler)); +#ifdef _WIN32 + FlushInstructionCache(GetCurrentProcess(), g_executable + offset, ThunkBytes); +#else + __builtin___clear_cache(reinterpret_cast(g_executable + offset), reinterpret_cast(g_executable + offset + ThunkBytes)); +#endif + return g_executable + offset; +} + +} + +static_assert(offsetof(ImportRecord, target) == 0); + +extern "C" { + +void APS5_VABI CallTraceRecord_nid_no_patch(const void* record, std::uint64_t returnAddress) { + static_cast(record)->calls.fetch_add(1, std::memory_order_relaxed); + const std::uint64_t sequence = g_sequence.fetch_add(1, std::memory_order_relaxed) + 1; + RingSlot& slot = g_ring.load(std::memory_order_relaxed)[sequence % RingCapacity]; + slot.sequence.store(0, std::memory_order_release); + slot.thread.store(CurrentThread(), std::memory_order_relaxed); + slot.returnAddress.store(returnAddress, std::memory_order_relaxed); + slot.record.store(static_cast(record), std::memory_order_relaxed); + slot.sequence.store(sequence, std::memory_order_release); +} + +void* TraceImport_nid_no_patch(void* target, const char* name, const char* library) { + if (target == nullptr || name == nullptr || library == nullptr || !Selected(library)) return target; + std::lock_guard lock(g_mutex); + if (g_ring.load(std::memory_order_relaxed) == nullptr) { + MapThunkArea(); + g_records.store(new ImportRecord[RecordCapacity], std::memory_order_release); + g_ring.store(new RingSlot[RingCapacity], std::memory_order_release); +#ifdef __linux__ + InstallAbortReport(); +#endif + } +#ifdef __linux__ + SnapshotModules(); +#endif + if (const auto existing = g_thunkByTarget.find(target); existing != g_thunkByTarget.end()) return existing->second; + const std::size_t index = g_recordCount.load(std::memory_order_relaxed); + if (index == RecordCapacity) throw std::runtime_error("call trace: thunk area exhausted"); + ImportRecord& record = g_records.load(std::memory_order_relaxed)[index]; + record.target = target; + record.name = name; + record.library = library; + void* thunk = WriteThunk(record, index); + g_thunkByTarget.emplace(target, thunk); + g_recordCount.store(index + 1, std::memory_order_release); + return thunk; +} + +std::size_t CopyCallTrace_nid_no_patch(CallTraceEntry* entries, std::size_t capacity) { + const RingSlot* ring = g_ring.load(std::memory_order_acquire); + if (entries == nullptr || capacity == 0 || ring == nullptr) return 0; + const std::uint64_t last = g_sequence.load(std::memory_order_acquire); + const std::uint64_t span = std::min({last, capacity, RingCapacity}); + std::size_t count = 0; + for (std::uint64_t sequence = last - span + 1; sequence <= last; ++sequence) { + const RingSlot& slot = ring[sequence % RingCapacity]; + if (slot.sequence.load(std::memory_order_acquire) != sequence) continue; + const auto* record = slot.record.load(std::memory_order_relaxed); + const CallTraceEntry entry{sequence, slot.thread.load(std::memory_order_relaxed), slot.returnAddress.load(std::memory_order_relaxed), record->name.c_str(), record->library.c_str()}; + if (slot.sequence.load(std::memory_order_acquire) != sequence) continue; + entries[count++] = entry; + } + return count; +} + +std::size_t CopyThreadCallTrace_nid_no_patch(std::uint64_t thread, CallTraceEntry* entries, std::size_t capacity) { + const RingSlot* ring = g_ring.load(std::memory_order_acquire); + if (entries == nullptr || capacity == 0 || ring == nullptr) return 0; + const std::uint64_t last = g_sequence.load(std::memory_order_acquire); + const std::uint64_t span = std::min(last, RingCapacity); + std::size_t count = 0; + for (std::uint64_t sequence = last; sequence > last - span && count < capacity; --sequence) { + const RingSlot& slot = ring[sequence % RingCapacity]; + if (slot.sequence.load(std::memory_order_acquire) != sequence || slot.thread.load(std::memory_order_relaxed) != thread) continue; + const auto* record = slot.record.load(std::memory_order_relaxed); + const CallTraceEntry entry{sequence, thread, slot.returnAddress.load(std::memory_order_relaxed), record->name.c_str(), record->library.c_str()}; + if (slot.sequence.load(std::memory_order_acquire) != sequence) continue; + entries[count++] = entry; + } + std::reverse(entries, entries + count); + return count; +} + +std::size_t CopyCallCounts_nid_no_patch(CallTraceCount* entries, std::size_t capacity, std::uint64_t* total) { + if (total != nullptr) *total = g_sequence.load(std::memory_order_acquire); + if (entries == nullptr || capacity == 0) return 0; + const std::size_t recorded = g_recordCount.load(std::memory_order_acquire); + const ImportRecord* records = g_records.load(std::memory_order_acquire); + std::size_t count = 0; + for (std::size_t index = 0; index < recorded; ++index) { + const ImportRecord& record = records[index]; + const std::uint64_t calls = record.calls.load(std::memory_order_relaxed); + if (calls == 0 || (count == capacity && calls <= entries[count - 1].calls)) continue; + std::size_t position = count < capacity ? count++ : count - 1; + while (position > 0 && entries[position - 1].calls < calls) { + entries[position] = entries[position - 1]; + --position; + } + entries[position] = {calls, record.name.c_str(), record.library.c_str()}; + } + return count; +} + +} + +asm(".text\n" + APS5_ASM_FUNCTION("CallTraceEnter_nid_no_patch") + " push %rdi\n" + " push %rsi\n" + " push %rdx\n" + " push %rcx\n" + " push %r8\n" + " push %r9\n" + " push %rax\n" + " push %r10\n" + " push %r11\n" + " sub $128, %rsp\n" + " movdqu %xmm0, 0(%rsp)\n" + " movdqu %xmm1, 16(%rsp)\n" + " movdqu %xmm2, 32(%rsp)\n" + " movdqu %xmm3, 48(%rsp)\n" + " movdqu %xmm4, 64(%rsp)\n" + " movdqu %xmm5, 80(%rsp)\n" + " movdqu %xmm6, 96(%rsp)\n" + " movdqu %xmm7, 112(%rsp)\n" + " mov %r11, %rdi\n" + " mov 200(%rsp), %rsi\n" + " call CallTraceRecord_nid_no_patch\n" + " movdqu 0(%rsp), %xmm0\n" + " movdqu 16(%rsp), %xmm1\n" + " movdqu 32(%rsp), %xmm2\n" + " movdqu 48(%rsp), %xmm3\n" + " movdqu 64(%rsp), %xmm4\n" + " movdqu 80(%rsp), %xmm5\n" + " movdqu 96(%rsp), %xmm6\n" + " movdqu 112(%rsp), %xmm7\n" + " add $128, %rsp\n" + " pop %r11\n" + " pop %r10\n" + " pop %rax\n" + " pop %r9\n" + " pop %r8\n" + " pop %rcx\n" + " pop %rdx\n" + " pop %rsi\n" + " pop %rdi\n" + " jmp *(%r11)\n" + APS5_ASM_FUNCTION_END("CallTraceEnter_nid_no_patch")); +#endif 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/Apr/src/Apr.cpp b/core/libs/prx/libkernel/Apr/src/Apr.cpp index 67847b576f..42e4274d9e 100644 --- a/core/libs/prx/libkernel/Apr/src/Apr.cpp +++ b/core/libs/prx/libkernel/Apr/src/Apr.cpp @@ -166,7 +166,7 @@ void _readFile(const Apr::ReadFileCommand& command) { const GuestArena::HostWrite destination(reinterpret_cast(command.destination), command.size); if (!destination.Open()) throw std::runtime_error("APR: the read destination of " + file.path.string() + " is not writable guest memory"); #ifdef _WIN32 - std::array staging; + std::vector staging(static_cast(std::min(command.size, 64u * 1024u))); std::uint64_t read = 0; while (read < command.size) { const auto wanted = static_cast(std::min(command.size - read, staging.size())); diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 46d29099ff..8613c79da0 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -2,6 +2,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Unimplemented.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Apr/src/Apr.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Equeue/Equeue.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Equeue/src/EdgeWatch.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Equeue/src/EventAccessors.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/ArchivedFile.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/DescriptorControl.cpp @@ -73,6 +74,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/System/src/SignalSets.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Notification.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/UserContext.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Time/SleepWake.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Time/Time.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Time/TimedWait.cpp ${CMAKE_CURRENT_SOURCE_DIR}/AppMetadata/src/AppMetadata.cpp @@ -108,7 +110,7 @@ if(BUILD_TESTING) configure_windows_unwind(guest_apr_read_tests) set(aprReadCases host direct eof errors streams) if(WIN32) - list(APPEND aprReadCases fault) + list(APPEND aprReadCases fault stack) endif() foreach(aprReadCase IN LISTS aprReadCases) set(aprReadRun ${CMAKE_CURRENT_BINARY_DIR}/apr_read_${aprReadCase}) @@ -273,6 +275,30 @@ if(BUILD_TESTING) set_tests_properties(guest_dynamic_loader_fixed_base PROPERTIES WORKING_DIRECTORY "$" RESOURCE_LOCK guest_dynamic_loader_files) endif() + file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/variant_names") + file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/variant_names/guest_module_fixture.sprx.GUEST.PRX" "not a module\n") + add_custom_command(TARGET guest_module_fixture POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy "$" + "${CMAKE_CURRENT_BINARY_DIR}/variant_names/guest_module_fixture.elf.GUEST.PRX" + VERBATIM) + if(NOT WIN32) + add_custom_command(TARGET guest_module_fixture POST_BUILD + COMMAND ${CMAKE_COMMAND} -E create_symlink missing.guest.prx + "${CMAKE_CURRENT_BINARY_DIR}/variant_names/guest_module_fixture.zprx.GUEST.PRX" + VERBATIM) + endif() + add_test_executable(guest_dynamic_loader_variants_tests ${LIBS_INCLUDE_DIR}/tests/GuestDynamicLoaderVariants.cpp) + target_include_directories(guest_dynamic_loader_variants_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_dynamic_loader_variants_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_dynamic_loader_variants_tests) + add_dependencies(guest_dynamic_loader_variants_tests guest_module_fixture) + add_test(NAME guest_dynamic_loader_variant_names COMMAND guest_dynamic_loader_variants_tests) + set_tests_properties(guest_dynamic_loader_variant_names PROPERTIES TIMEOUT 15 + WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/variant_names") + if(WIN32) + set_tests_properties(guest_dynamic_loader_variant_names PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_module_info_tests ${LIBS_INCLUDE_DIR}/tests/GuestModuleInfo.cpp) target_include_directories(guest_module_info_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_module_info_tests PRIVATE libkernel libc) @@ -309,6 +335,11 @@ if(BUILD_TESTING) target_link_libraries(guest_record_lock_owners_tests PRIVATE libkernel libc) add_test(NAME guest_record_lock_owners COMMAND guest_record_lock_owners_tests) set_tests_properties(guest_record_lock_owners PROPERTIES TIMEOUT 20) + add_test_executable(guest_path_errors_tests ${LIBS_INCLUDE_DIR}/tests/GuestPathErrors.cpp) + target_include_directories(guest_path_errors_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_path_errors_tests PRIVATE libkernel libc) + add_test(NAME guest_path_errors COMMAND guest_path_errors_tests) + set_tests_properties(guest_path_errors PROPERTIES TIMEOUT 20) add_test_executable(guest_host_standard_error_tests ${LIBS_INCLUDE_DIR}/tests/GuestHostStandardError.cpp) target_include_directories(guest_host_standard_error_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_host_standard_error_tests PRIVATE libkernel libc) @@ -320,6 +351,15 @@ if(BUILD_TESTING) add_test(NAME guest_closed_standard_descriptors COMMAND guest_closed_standard_descriptors_tests) set_tests_properties(guest_closed_standard_descriptors PROPERTIES TIMEOUT 10) endif() + if(WIN32) + add_test_executable(guest_host_standard_error_windows_tests ${LIBS_INCLUDE_DIR}/tests/GuestHostStandardErrorWindows.cpp) + target_include_directories(guest_host_standard_error_windows_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_host_standard_error_windows_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_host_standard_error_windows_tests) + add_test(NAME guest_host_standard_error_windows COMMAND guest_host_standard_error_windows_tests) + set_tests_properties(guest_host_standard_error_windows PROPERTIES TIMEOUT 10 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_truncate_tests ${LIBS_INCLUDE_DIR}/tests/GuestTruncate.cpp) target_include_directories(guest_truncate_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_truncate_tests PRIVATE libkernel libc) @@ -340,6 +380,16 @@ if(BUILD_TESTING) set_tests_properties(guest_truncate_race PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_termios_speed_tests ${LIBS_INCLUDE_DIR}/tests/GuestTermiosSpeed.cpp) + target_include_directories(guest_termios_speed_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_termios_speed_tests PRIVATE libkernel libc ${CMAKE_DL_LIBS}) + configure_windows_unwind(guest_termios_speed_tests) + add_test(NAME guest_termios_speed COMMAND guest_termios_speed_tests $) + if(WIN32) + set_tests_properties(guest_termios_speed PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_terminal_tests ${LIBS_INCLUDE_DIR}/tests/GuestTerminal.cpp) target_include_directories(guest_terminal_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_terminal_tests PRIVATE libkernel libc) @@ -721,6 +771,18 @@ if(BUILD_TESTING) set_tests_properties(guest_nanosleep_timer_self_test PROPERTIES TIMEOUT 30 ENVIRONMENT "APS5_TRACE_TIMERS=1" ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_sleep_wake_tests ${LIBS_INCLUDE_DIR}/tests/GuestSleepWake.cpp) + target_include_directories(guest_sleep_wake_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_sleep_wake_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_sleep_wake_tests) + add_test(NAME guest_sleep_wake COMMAND guest_sleep_wake_tests) + set_tests_properties(guest_sleep_wake PROPERTIES TIMEOUT 20) + if(WIN32) + add_test(NAME guest_sleep_wake_coarse COMMAND guest_sleep_wake_tests) + set_tests_properties(guest_sleep_wake_coarse PROPERTIES TIMEOUT 20 ENVIRONMENT "APS5_COARSE_TIMED_WAITS=1") + set_tests_properties(guest_sleep_wake guest_sleep_wake_coarse PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_pthread_sem_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadSem.cpp) target_include_directories(guest_pthread_sem_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_pthread_sem_tests PRIVATE libkernel libc) @@ -1322,6 +1384,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) @@ -1388,9 +1460,10 @@ if(BUILD_TESTING) target_link_libraries(guest_socket_pair_tests PRIVATE libkernel libc) configure_windows_unwind(guest_socket_pair_tests) add_test(NAME guest_socket_pair COMMAND guest_socket_pair_tests) - set_tests_properties(guest_socket_pair PROPERTIES TIMEOUT 20) + add_test(NAME guest_recvmsg_memory COMMAND guest_socket_pair_tests recvmsg-memory) + set_tests_properties(guest_socket_pair guest_recvmsg_memory PROPERTIES TIMEOUT 20) if(WIN32) - set_tests_properties(guest_socket_pair PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_socket_pair guest_recvmsg_memory PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test_executable(guest_kernel_app_archive_tests ${LIBS_INCLUDE_DIR}/tests/GuestKernelAppArchive.cpp) @@ -1405,6 +1478,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) @@ -1613,12 +1696,17 @@ if(BUILD_TESTING) set_tests_properties(guest_directory_rewind PROPERTIES TIMEOUT 15 ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() - if(NOT WIN32) - add_test_executable(guest_directory_entry_tests ${LIBS_INCLUDE_DIR}/tests/GuestDirectoryEntries.cpp) - target_include_directories(guest_directory_entry_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_directory_entry_tests PRIVATE libkernel libc) - add_test(NAME guest_directory_entries COMMAND guest_directory_entry_tests) - else() + add_test_executable(guest_directory_entry_tests ${LIBS_INCLUDE_DIR}/tests/GuestDirectoryEntries.cpp) + target_include_directories(guest_directory_entry_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_directory_entry_tests PRIVATE libkernel libc) + add_test(NAME guest_directory_entries COMMAND guest_directory_entry_tests) + if(WIN32) + configure_windows_unwind(guest_directory_entry_tests) + set_tests_properties(guest_directory_entries PROPERTIES TIMEOUT 15 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + if(MINGW) + target_compile_options(guest_directory_entry_tests PRIVATE -fasynchronous-unwind-tables) + endif() add_test_executable(guest_directory_close_tests ${LIBS_INCLUDE_DIR}/tests/GuestDirectoryClose.cpp) target_include_directories(guest_directory_close_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_directory_close_tests PRIVATE libkernel libc) @@ -1640,6 +1728,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) @@ -1649,6 +1747,16 @@ if(BUILD_TESTING) set_tests_properties(guest_file_vector PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_file_vector_memory_tests ${LIBS_INCLUDE_DIR}/tests/GuestFileVectorMemory.cpp) + target_include_directories(guest_file_vector_memory_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_file_vector_memory_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_file_vector_memory_tests) + add_test(NAME guest_file_vector_memory COMMAND guest_file_vector_memory_tests) + set_tests_properties(guest_file_vector_memory PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_file_vector_memory PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_fstat_fault_tests ${LIBS_INCLUDE_DIR}/tests/GuestFstatFault.cpp) target_include_directories(guest_fstat_fault_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_fstat_fault_tests PRIVATE libkernel libc) @@ -1659,6 +1767,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) @@ -1720,6 +1848,20 @@ 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_system_call_tests ${LIBS_INCLUDE_DIR}/tests/GuestRaiseInSystemCall.cpp) + target_include_directories(guest_raise_in_system_call_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_raise_in_system_call_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_raise_in_system_call_tests) + add_test(NAME guest_raise_in_system_call COMMAND guest_raise_in_system_call_tests) + set_tests_properties(guest_raise_in_system_call PROPERTIES TIMEOUT 30 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + 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) @@ -1759,3 +1901,20 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +if(BUILD_TESTING) + add_test_executable(guest_sleep_cancel_tests ${LIBS_INCLUDE_DIR}/tests/GuestSleepCancel.cpp) + target_include_directories(guest_sleep_cancel_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_sleep_cancel_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_sleep_cancel_tests) + foreach(sleepCase IN ITEMS blocked disabled normal pending) + add_test(NAME guest_sleep_cancel_${sleepCase} COMMAND guest_sleep_cancel_tests ${sleepCase}) + add_test(NAME guest_sleep_cancel_${sleepCase}_coarse COMMAND guest_sleep_cancel_tests ${sleepCase}) + set_tests_properties(guest_sleep_cancel_${sleepCase} guest_sleep_cancel_${sleepCase}_coarse PROPERTIES TIMEOUT 20) + set_tests_properties(guest_sleep_cancel_${sleepCase}_coarse PROPERTIES ENVIRONMENT "APS5_COARSE_TIMED_WAITS=1") + if(WIN32) + set_tests_properties(guest_sleep_cancel_${sleepCase} guest_sleep_cancel_${sleepCase}_coarse PROPERTIES + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() + endforeach() +endif() diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index 8d5b7996ad..e3bddd7224 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -326,13 +327,63 @@ void ZeroReusedPhysical(std::uint64_t start, std::size_t bytes) { } #endif +#ifdef _WIN32 +bool SectionPoolEnabled() { + static const bool enabled = std::getenv("APS5_NO_DIRECT_MEMORY_SECTION_POOL") == nullptr; + return enabled; +} + +class SectionPool { +public: + static SectionPool& Get() { + static auto* pool = new SectionPool; + return *pool; + } + + HANDLE Take(std::size_t bytes) { + std::lock_guard lock(mutex); + const auto found = sections.find(bytes); + if (found == sections.end() || found->second.empty()) return nullptr; + const auto section = found->second.back(); + found->second.pop_back(); + --count; + pooledBytes -= bytes; + return section; + } + + bool Keep(HANDLE section, std::size_t bytes) noexcept { + if (bytes > MaxSectionBytes) return false; + std::lock_guard lock(mutex); + if (count == MaxSections || bytes > MaxPooledBytes - pooledBytes) return false; + try { + sections[bytes].push_back(section); + } catch (const std::bad_alloc&) { + return false; + } + ++count; + pooledBytes += bytes; + return true; + } + +private: + static constexpr std::size_t MaxSections = 256; + static constexpr std::size_t MaxSectionBytes = 16u << 20; + static constexpr std::size_t MaxPooledBytes = 256u << 20; + std::mutex mutex; + std::map> sections; + std::size_t count = 0; + std::size_t pooledBytes = 0; +}; +#endif + class PhysicalBacking { public: PhysicalBacking(std::size_t bytes, int memoryType, std::uint64_t start) : memoryType(memoryType), bytes(bytes) { #ifdef _WIN32 static_cast(start); const auto size = static_cast(bytes); - section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE | SEC_RESERVE, static_cast(size >> 32), static_cast(size), nullptr); + if (SectionPoolEnabled()) section = SectionPool::Get().Take(bytes); + if (!section) section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE | SEC_RESERVE, static_cast(size >> 32), static_cast(size), nullptr); if (!section) { const auto error = static_cast(GetLastError()); char message[96]; @@ -364,8 +415,12 @@ public: ~PhysicalBacking() { #ifdef _WIN32 - GuestArena::GuestArenaForgetSection_nid_postfix(section); - CloseHandle(section); + if (!SectionPoolEnabled()) { + GuestArena::GuestArenaForgetSection_nid_postfix(section); + CloseHandle(section); + } else if (!GuestArena::GuestArenaForgetReusableSection_nid_no_patch(section) || !SectionPool::Get().Keep(section, bytes)) { + CloseHandle(section); + } #else if (hostWriteView != nullptr) ::munmap(hostWriteView, bytes); #endif @@ -662,7 +717,8 @@ void RequireAliasesUnpinned(GuestAllocations::Mutation& mutation, std::uint64_t std::vector> aliases; { std::lock_guard lock(g_directLock); - for (const auto& [base, mapping] : g_directMappings) { + for (const auto base : MappingsSharingPhys(phys, phys + len)) { + const auto& mapping = g_directMappings.at(base); const auto first = std::max(phys, mapping.phys); const auto last = std::min(phys + len, mapping.phys + (mapping.end - base)); if (first < last) aliases.emplace_back(base + (first - mapping.phys), last - first); diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp index 2e0f21cf18..604e85b34c 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp @@ -19,6 +19,9 @@ using HostSharedMemory = int; int DirectMemoryAlloc(int64_t searchStart, int64_t searchEnd, size_t len, size_t alignment, int memoryType, int64_t* physOut); void DirectMemoryFree(int64_t start, size_t len); bool DirectMemoryCheckedFree(int64_t start, size_t len); +bool DirectMemoryClaimRelease(int64_t start, size_t len, bool requireAllocated); +void DirectMemoryFinishRelease(int64_t start, size_t len); +void DirectMemoryAbandonRelease(int64_t start, size_t len) noexcept; void CreateDirectMemoryBacking(int64_t start, size_t len, int memoryType); bool QueryDirectMapping(std::uintptr_t address, std::uintptr_t* start, std::uintptr_t* end, std::uint64_t* offset, int* memoryType); void ForgetDirectMemory(int64_t start, size_t len); @@ -32,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 d8e6adeb42..79d07266cb 100644 --- a/core/libs/prx/libkernel/DirectMemory/Export.cpp +++ b/core/libs/prx/libkernel/DirectMemory/Export.cpp @@ -93,6 +93,16 @@ void ApplyRangeName(uintptr_t address, VirtualQueryInfo* info) { if (next != g_rangeNames.end()) info->end = std::min(info->end, next->first); } +void ReleaseClaimedDirectMemory(int64_t start, size_t len) { + try { + UnmapDirectMemoryViews(DirectMemoryViews(start, len)); + } catch (...) { + DirectMemoryAbandonRelease(start, len); + throw; + } + DirectMemoryFinishRelease(start, len); +} + std::mutex g_prtLock; PrtAperture g_prtApertures[PRT_APERTURE_COUNT] = {}; @@ -211,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; @@ -317,9 +328,8 @@ int APS5_VABI sceKernelReleaseDirectMemory(int64_t start, size_t len) { if ((static_cast(start) & (PS5_PAGE_SIZE - 1)) != 0 || (len & (PS5_PAGE_SIZE - 1)) != 0 || len > DIRECT_MEMORY_SIZE || static_cast(start) > DIRECT_MEMORY_SIZE - len) return SCE_KERNEL_ERROR_EINVAL; - const auto views = DirectMemoryViews(start, len); - DirectMemoryFree(start, len); - UnmapDirectMemoryViews(views); + if (!DirectMemoryClaimRelease(start, len, false)) return 0; + ReleaseClaimedDirectMemory(start, len); return 0; } @@ -408,9 +418,8 @@ int APS5_VABI sceKernelVirtualQuery(const void* addr, int flags, VirtualQueryInf int APS5_VABI sceKernelCheckedReleaseDirectMemory(int64_t start, size_t len) { if (start < 0 || (static_cast(start) & (PS5_PAGE_SIZE - 1)) != 0 || (len & (PS5_PAGE_SIZE - 1)) != 0) return SCE_KERNEL_ERROR_EINVAL; if (len == 0) return 0; - const auto views = DirectMemoryViews(start, len); - if (!DirectMemoryCheckedFree(start, len)) return SCE_KERNEL_ERROR_ENOENT; - UnmapDirectMemoryViews(views); + if (!DirectMemoryClaimRelease(start, len, true)) return SCE_KERNEL_ERROR_ENOENT; + ReleaseClaimedDirectMemory(start, len); return 0; } diff --git a/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp b/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp index fc082fb8e9..54cdbaf12d 100644 --- a/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp +++ b/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp @@ -1,6 +1,7 @@ #include "prx/libkernel/DirectMemory/MemoryPool.hpp" #include "prx/libkernel/DirectMemory/DirectMemory.hpp" #include +#include #include #include #include @@ -87,6 +88,40 @@ struct PhysicalMemoryPool { bool CheckedFree(uint64_t start, size_t len) { return _release(start, len, true); } + bool ClaimRelease(uint64_t start, size_t len, bool requireAllocated) { + uint64_t lo, hi; + if (!PageSpan(start, len, lo, hi)) return false; + std::unique_lock lock(_mutex); + if (requireAllocated) { + if (!_allUsed(static_cast(lo / kPage), static_cast((hi - lo) / kPage)) || _claimed(lo, hi)) return false; + } else { + _claimFinished.wait(lock, [&] { return !_claimed(lo, hi); }); + } + _claims.emplace(lo, hi); + return true; + } + + void FinishRelease(uint64_t start, size_t len) { + uint64_t lo, hi; + if (!PageSpan(start, len, lo, hi)) return; + { + std::lock_guard lock(_mutex); + _claims.erase(lo); + (void)_releaseLocked(lo, hi, false); + } + _claimFinished.notify_all(); + } + + void AbandonRelease(uint64_t start, size_t len) noexcept { + uint64_t lo, hi; + if (!PageSpan(start, len, lo, hi)) return; + { + std::lock_guard lock(_mutex); + _claims.erase(lo); + } + _claimFinished.notify_all(); + } + bool Find(uint64_t offset, bool findNext, int64_t* start, int64_t* end, int* memoryType) { if (!start || !end || !memoryType || offset >= kPoolBytes) return false; std::lock_guard lock(_mutex); @@ -118,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); @@ -149,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) { @@ -211,13 +246,21 @@ private: _mark(static_cast(start / kPage), static_cast(len / kPage), true); } + bool _claimed(uint64_t lo, uint64_t hi) const { + std::map::const_iterator it = _claims.lower_bound(hi); + return it != _claims.begin() && std::prev(it)->second > lo; + } + bool _release(uint64_t start, size_t len, bool requireAllocated) { uint64_t lo, hi; if (!PageSpan(start, len, lo, hi)) return false; + std::lock_guard lock(_mutex); + return _releaseLocked(lo, hi, requireAllocated); + } + + bool _releaseLocked(uint64_t lo, uint64_t hi, bool requireAllocated) { const size_t first = static_cast(lo / kPage); const size_t count = static_cast((hi - lo) / kPage); - - std::lock_guard lock(_mutex); if (requireAllocated && !_allUsed(first, count)) return false; const bool splitLo = _splitAt(lo); @@ -256,8 +299,10 @@ private: } std::mutex _mutex; + std::condition_variable _claimFinished; bool _used[kNumPages] = {}; BlockMap _blocks; + std::map _claims; }; } @@ -278,6 +323,18 @@ bool DirectMemoryCheckedFree(int64_t start, size_t len) { return PhysicalMemoryPool::Instance().CheckedFree(static_cast(start), len); } +bool DirectMemoryClaimRelease(int64_t start, size_t len, bool requireAllocated) { + return PhysicalMemoryPool::Instance().ClaimRelease(static_cast(start), len, requireAllocated); +} + +void DirectMemoryFinishRelease(int64_t start, size_t len) { + PhysicalMemoryPool::Instance().FinishRelease(static_cast(start), len); +} + +void DirectMemoryAbandonRelease(int64_t start, size_t len) noexcept { + PhysicalMemoryPool::Instance().AbandonRelease(static_cast(start), len); +} + bool DirectMemoryFind(int64_t offset, bool findNext, int64_t* start, int64_t* end, int* memoryType) { return PhysicalMemoryPool::Instance().Find(static_cast(offset), findNext, start, end, memoryType); } @@ -286,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 22d850be74..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 @@ -98,8 +99,15 @@ void KernelEqueuePrivate::WakePollers() { void KernelEqueuePrivate::WaitForDescriptors(std::unique_lock& lock, std::uint64_t deadlineNanos) { std::vector interests; + std::vector> held; for (const auto& e : m_events) { - if (e.enabled && e.filter.pollFunc != nullptr) { + if (!e.enabled || e.filter.pollFunc == nullptr) { + continue; + } + if (e.filter.waitDescriptor >= 0) { + interests.push_back({e.filter.waitDescriptor, false}); + held.push_back(e.filter.owner); + } else { interests.push_back({static_cast(e.event.ident), e.event.filter == EVFILT_WRITE}); } } @@ -623,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/Equeue/Equeue.hpp b/core/libs/prx/libkernel/Equeue/Equeue.hpp index 61096a5136..860dabf3e5 100644 --- a/core/libs/prx/libkernel/Equeue/Equeue.hpp +++ b/core/libs/prx/libkernel/Equeue/Equeue.hpp @@ -55,6 +55,7 @@ struct KernelFilter { EqueueResetFunc resetFunc = nullptr; EqueueDeleteFunc deleteEventFunc = nullptr; EqueuePollFunc pollFunc = nullptr; + int waitDescriptor = -1; }; struct KernelEqueueEvent { diff --git a/core/libs/prx/libkernel/Equeue/include/EdgeWatch.hpp b/core/libs/prx/libkernel/Equeue/include/EdgeWatch.hpp new file mode 100644 index 0000000000..230939fd06 --- /dev/null +++ b/core/libs/prx/libkernel/Equeue/include/EdgeWatch.hpp @@ -0,0 +1,29 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_EQUEUE_EDGEWATCH_HPP +#define CORE_LIBS_PRX_LIBKERNEL_EQUEUE_EDGEWATCH_HPP + +#include +#include + +namespace EdgeWatch { + +class Watch { +public: + static std::shared_ptr Open(std::intptr_t hostDescriptor, bool write); + + explicit Watch(int descriptor) : m_descriptor(descriptor) {} + ~Watch(); + Watch(const Watch&) = delete; + Watch& operator=(const Watch&) = delete; + + bool TakeEdge(); + int Descriptor() const { return m_descriptor; } + + bool armed = true; + +private: + int m_descriptor; +}; + +} + +#endif diff --git a/core/libs/prx/libkernel/Equeue/src/EdgeWatch.cpp b/core/libs/prx/libkernel/Equeue/src/EdgeWatch.cpp new file mode 100644 index 0000000000..60e001c6b3 --- /dev/null +++ b/core/libs/prx/libkernel/Equeue/src/EdgeWatch.cpp @@ -0,0 +1,55 @@ +#include "prx/libkernel/Equeue/include/EdgeWatch.hpp" +#include "prx/libc/include/General.hpp" + +#include +#include +#include + +#ifdef __linux__ +#include +#include +#endif + +namespace EdgeWatch { + +#ifdef __linux__ + +std::shared_ptr Watch::Open(std::intptr_t hostDescriptor, bool write) { + const int descriptor = ::epoll_create1(EPOLL_CLOEXEC); + if (descriptor < 0) { + throw std::runtime_error("kevent: EV_CLEAR: epoll_create1 failed, host errno " + std::to_string(errno)); + } + auto watch = std::make_shared(descriptor); + epoll_event interest{}; + interest.events = EPOLLET | (write ? EPOLLOUT : (EPOLLIN | EPOLLRDHUP)); + if (::epoll_ctl(descriptor, EPOLL_CTL_ADD, static_cast(hostDescriptor), &interest) != 0) { + throw std::runtime_error("kevent: EV_CLEAR: watching host descriptor " + std::to_string(hostDescriptor) + " failed, host errno " + std::to_string(errno)); + } + return watch; +} + +Watch::~Watch() { + ::close(m_descriptor); +} + +bool Watch::TakeEdge() { + epoll_event ready{}; + return ::epoll_wait(m_descriptor, &ready, 1, 0) > 0; +} + +#else + +std::shared_ptr Watch::Open(std::intptr_t, bool) { + NotImplemented_nid_no_patch("kevent: EV_CLEAR on a socket or a pipe off Linux"); + return nullptr; +} + +Watch::~Watch() = default; + +bool Watch::TakeEdge() { + return false; +} + +#endif + +} diff --git a/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp b/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp index b6bbf66d7e..04fe98539f 100644 --- a/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp +++ b/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp @@ -368,7 +368,9 @@ int OpenHostWatch(int descriptor, std::uint32_t notes, HostWatch& host) { throw std::runtime_error("EVFILT_VNODE: reading the watched file failed, error=" + std::to_string(error)); } try { - host.parent = WatchParent(host.last.path); + if (!IsDeleted(host.last)) { + host.parent = WatchParent(host.last.path); + } if (host.directory) { host.entries = WatchContents(host.last.path, FILE_NOTIFY_CHANGE_FILE_NAME, "entries"); host.subdirectories = WatchContents(host.last.path, FILE_NOTIFY_CHANGE_DIR_NAME, "subdirectories"); diff --git a/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp b/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp index d03afadb9e..0e7f1e05e0 100644 --- a/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp +++ b/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp @@ -1,4 +1,5 @@ #include "prx/libkernel/Equeue/Equeue.hpp" +#include "prx/libkernel/Equeue/include/EdgeWatch.hpp" #include "prx/libc/include/General.hpp" #include "prx/libkernel/File/include/File.hpp" #include "prx/libkernel/File/include/HostStreams.hpp" @@ -35,6 +36,7 @@ constexpr int GUEST_EFAULT = 14; constexpr int GUEST_EINVAL = 22; constexpr int GUEST_ENFILE = 23; constexpr int GUEST_EMFILE = 24; +constexpr int GUEST_EPIPE = 32; constexpr int KQ_NEVENTS = 8; constexpr std::uint32_t NOTE_FFCTRLMASK = 0xc0000000; @@ -132,9 +134,6 @@ std::int64_t DescriptorLowWater(const KernelEvent& change) { if (change.data < 1) { NotImplemented_nid_no_patch("kevent: NOTE_LOWAT below 1 on a socket"); } - if (change.filter == EVFILT_WRITE && change.data > 1) { - NotImplemented_nid_no_patch("kevent: NOTE_LOWAT above 1 on EVFILT_WRITE"); - } if (change.data > 1 && GuestSockets::Type(static_cast(change.ident)) != 1) { NotImplemented_nid_no_patch("kevent: NOTE_LOWAT above 1 on a socket that is not a stream"); } @@ -152,6 +151,44 @@ int AddStandardInputKnote(const KernelEqueueRef& queue, KernelEvent& change, std return GuestErrno(queue->AddEvent(event)); } +bool PollEdge(KernelEqueueEvent* e) { + auto& watch = *static_cast(e->filter.data); + if (watch.TakeEdge()) { + watch.armed = true; + } + if (!watch.armed) { + return false; + } + std::int64_t data = 0; + bool eof = false; + if (!GuestSockets::Ready(static_cast(e->event.ident), e->event.filter == EVFILT_WRITE, &data, &eof)) { + watch.armed = false; + return false; + } + if (!eof && data < e->lowWater && !GuestSockets::Listening(static_cast(e->event.ident))) { + watch.armed = false; + return false; + } + e->event.data = static_cast(data); + e->event.flags = static_cast(eof ? (e->event.flags | EV_EOF) : (e->event.flags & ~EV_EOF)); + return true; +} + +void ResetEdge(KernelEqueueEvent* e) { + static_cast(e->filter.data)->armed = false; + e->triggered = false; +} + +void WatchEdges(KernelEqueueEvent& event, bool socket) { + const int descriptor = static_cast(event.event.ident); + auto watch = EdgeWatch::Watch::Open(socket ? GuestSockets::NativeDescriptor(descriptor) : descriptor, event.event.filter == EVFILT_WRITE); + event.filter.data = watch.get(); + event.filter.waitDescriptor = watch->Descriptor(); + event.filter.owner = std::move(watch); + event.filter.pollFunc = PollEdge; + event.filter.resetFunc = ResetEdge; +} + int AddDescriptorKnote(const KernelEqueueRef& queue, KernelEvent& change, std::uint16_t flags) { const int descriptor = static_cast(change.ident); if (IsKqueueDescriptor(descriptor)) { @@ -166,16 +203,20 @@ int AddDescriptorKnote(const KernelEqueueRef& queue, KernelEvent& change, std::u const bool socket = GuestSockets::IsOpen(descriptor); std::int64_t data = 0; bool eof = false; - const bool pipe = !socket && change.filter == EVFILT_READ && GuestSockets::PipeReadState(descriptor, &data, &eof); + bool writable = false; + const bool pipe = !socket && (change.filter == EVFILT_READ ? GuestSockets::PipeReadState(descriptor, &data, &eof) : GuestSockets::PipeWriteState(descriptor, &data, &eof, &writable)); if (!socket && !pipe) { - NotImplemented_nid_no_patch(change.filter == EVFILT_READ ? "kevent: EVFILT_READ on a descriptor that is neither a socket nor the read end of a pipe" : "kevent: EVFILT_WRITE on a non-socket descriptor"); + NotImplemented_nid_no_patch(change.filter == EVFILT_READ ? "kevent: EVFILT_READ on a descriptor that is neither a socket nor the read end of a pipe" : "kevent: EVFILT_WRITE on a descriptor that is neither a socket nor the write end of a pipe"); + } + if (pipe && change.filter == EVFILT_WRITE && eof) { + return GUEST_EPIPE; } const auto lowWater = DescriptorLowWater(change); - if ((flags & EV_CLEAR) != 0) { - NotImplemented_nid_no_patch(pipe ? "kevent: EV_CLEAR on a pipe" : "kevent: EV_CLEAR on a socket"); - } auto event = EqueueDescriptorEvent(descriptor, change.filter, flags, change.udata); event.lowWater = lowWater; + if ((flags & EV_CLEAR) != 0) { + WatchEdges(event, socket); + } change.fflags = 0; change.data = 0; return GuestErrno(queue->AddEvent(event)); @@ -266,6 +307,9 @@ int ModifyKnote(const KernelEqueueRef& queue, KernelEvent& change) { event.event.flags = static_cast(event.event.flags | EV_ONESHOT); event.triggered = true; } + if (event.enabled && event.filter.pollFunc == PollEdge) { + static_cast(event.filter.data)->armed = true; + } if (change.filter == EVFILT_USER) { TouchUserKnote(event, change); } else if (change.filter == EVFILT_VNODE) { diff --git a/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp b/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp index c2d2dab315..a6403ce990 100644 --- a/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp +++ b/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp @@ -18,6 +18,7 @@ static constexpr int SCE_OK = 0; static constexpr int SCE_KERNEL_ERROR_EPERM = 0x80020001; +static constexpr int SCE_KERNEL_ERROR_ENOENT = 0x80020002; static constexpr int SCE_KERNEL_ERROR_ESRCH = 0x80020003; static constexpr int SCE_KERNEL_ERROR_ENOMEM = 0x8002000C; static constexpr int SCE_KERNEL_ERROR_EACCES = 0x8002000D; @@ -146,6 +147,18 @@ int APS5_VABI sceKernelCreateEventFlag(KernelEventFlag* ef, const char* name, ui return SCE_OK; } +int APS5_VABI sceKernelOpenEventFlag(KernelEventFlag* ef, const char* name) { + if (!ef || !name) return SCE_KERNEL_ERROR_EINVAL; + std::shared_lock lock(eventFlagRegistryMutex); + for (const auto& [handle, flag] : eventFlagRegistry) { + std::lock_guard flagLock(flag->_mutex); + if (flag->_deleted || flag->_name != name) continue; + *ef = handle; + return SCE_OK; + } + return SCE_KERNEL_ERROR_ENOENT; +} + int APS5_VABI sceKernelDeleteEventFlag(KernelEventFlag ef) { if (!ef) return SCE_KERNEL_ERROR_ESRCH; const auto keepAlive = ResolveEventFlag(ef); 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..6978a7998a 100644 --- a/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp +++ b/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp @@ -5,8 +5,12 @@ #include #include +struct FileStat; + extern "C" const char* GetDirectoryDescriptorPath_nid_no_patch(int fd, int* error); +extern "C" std::uint64_t GetDirectoryDescriptorPosition_nid_no_patch(int fd); extern "C" int OpenInDirectory_nid_no_patch(int directory, const char* path, int flags, std::uint16_t mode); +extern "C" int StatInDirectory_nid_no_patch(int directory, const char* path, FileStat* sb, bool follow); namespace File { @@ -16,7 +20,7 @@ std::optional DirectoryDescriptorPath(int fd); void ForgetDirectoryDescriptor(int fd); void DuplicateDirectoryDescriptor(int from, int to); std::optional SeekDirectoryDescriptor(int fd, std::int64_t offset, int whence); -int ReadDirectoryDescriptor(int fd, char* buf, int nbytes); +int ReadDirectoryDescriptor(int fd, char* buf, int nbytes, std::int64_t* base = nullptr); } diff --git a/core/libs/prx/libkernel/File/include/FileFlags.hpp b/core/libs/prx/libkernel/File/include/FileFlags.hpp index 2423ccdacf..374abfc4c5 100644 --- a/core/libs/prx/libkernel/File/include/FileFlags.hpp +++ b/core/libs/prx/libkernel/File/include/FileFlags.hpp @@ -19,4 +19,13 @@ constexpr std::int32_t SCE_KERNEL_O_NONBLOCK = 0x00000004; constexpr std::int32_t SCE_KERNEL_O_DIRECT = 0x00010000; constexpr std::int32_t SCE_KERNEL_O_DIRECTORY = 0x00020000; +namespace File { + +constexpr bool InvalidOpenFlags(std::int32_t flags) { + constexpr std::int32_t createExclusiveDirectory = SCE_KERNEL_O_CREAT | SCE_KERNEL_O_EXCL | SCE_KERNEL_O_DIRECTORY; + return (flags & SCE_KERNEL_O_ACCMODE) == SCE_KERNEL_O_ACCMODE || (flags & createExclusiveDirectory) == createExclusiveDirectory; +} + +} + #endif diff --git a/core/libs/prx/libkernel/File/include/NativeStat.hpp b/core/libs/prx/libkernel/File/include/NativeStat.hpp index 5715f02119..b60abe0b89 100644 --- a/core/libs/prx/libkernel/File/include/NativeStat.hpp +++ b/core/libs/prx/libkernel/File/include/NativeStat.hpp @@ -7,6 +7,7 @@ namespace File { void FillFileStat(const std::filesystem::path& nativePath, FileStat* sb); +bool TryFillFileStat(const std::filesystem::path& nativePath, FileStat* sb); void FillFileStat(int nativeDescriptor, FileStat* sb); bool FillFileStatFromDescriptor(int fd, FileStat* sb); bool FillLinkStat(const std::filesystem::path& nativePath, FileStat* sb); diff --git a/core/libs/prx/libkernel/File/include/WindowsFileTime.hpp b/core/libs/prx/libkernel/File/include/WindowsFileTime.hpp index 07721951e2..9b3a4a27fd 100644 --- a/core/libs/prx/libkernel/File/include/WindowsFileTime.hpp +++ b/core/libs/prx/libkernel/File/include/WindowsFileTime.hpp @@ -38,6 +38,10 @@ public: static int Failure(DWORD error) { const auto condition = std::system_category().default_error_condition(static_cast(error)); +#ifdef ELOOP + if (error == ERROR_CANT_RESOLVE_FILENAME) errno = ELOOP; + else +#endif if (condition == std::errc::no_such_file_or_directory) errno = ENOENT; else if (condition == std::errc::permission_denied) errno = EACCES; else if (condition == std::errc::bad_file_descriptor) errno = EBADF; diff --git a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp index 82db339af5..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 { @@ -107,7 +109,7 @@ static_assert(sizeof(GuestLock) == 32); static_assert(offsetof(GuestLock, type) == 20); constexpr std::size_t OldGuestLockSize = 24; -enum class Kind { File, Directory, Pipe, Device, RandomDevice, ArchivedFile }; +enum class Kind { File, Directory, Pipe, Socket, Device, RandomDevice, ArchivedFile }; struct Entry { bool closeOnExec = false; @@ -127,6 +129,7 @@ const char* KindName(Kind kind) { case Kind::File: return "file"; case Kind::Directory: return "directory"; case Kind::Pipe: return "pipe"; + case Kind::Socket: return "socket"; case Kind::Device: return "character device"; case Kind::RandomDevice: return "random device"; case Kind::ArchivedFile: return "file read from app0.zar"; @@ -268,7 +271,8 @@ std::optional Classify(int fd, const char* function) { if (S_ISDIR(status.st_mode)) return Kind::Directory; if (S_ISFIFO(status.st_mode)) return Kind::Pipe; if (S_ISCHR(status.st_mode)) return Kind::Device; - throw std::runtime_error(std::string(function) + ": descriptor " + std::to_string(fd) + " is neither a file, a directory, a pipe nor a character device"); + if (S_ISSOCK(status.st_mode)) return Kind::Socket; + throw std::runtime_error(std::string(function) + ": descriptor " + std::to_string(fd) + " is neither a file, a directory, a pipe, a socket nor a character device"); } int HostStatus(int fd, const char* function) { @@ -345,7 +349,7 @@ int ChangeStatus(int fd, Kind kind, int mask, int value, const char* function) { auto& status = *Materialize(fd).status; const int next = (status & ~mask) | (value & mask); const int changed = status ^ next; - if ((changed & Asynchronous) && (kind == Kind::Pipe || kind == Kind::Device)) Unsupported(function, "O_ASYNC", kind, fd); + if ((changed & Asynchronous) && (kind == Kind::Pipe || kind == Kind::Socket || kind == Kind::Device)) Unsupported(function, "O_ASYNC", kind, fd); if ((changed & Synchronous) && kind == Kind::File) Unsupported(function, "changing O_FSYNC", kind, fd); ApplyHostStatus(fd, kind, changed, next, function); status = next; @@ -371,7 +375,7 @@ int RecordLock(int fd, int command, std::intptr_t argument) { GuestLock lock{}; std::memcpy(&lock, pointer, size); const auto kind = Classify(fd, "fcntl"); - if (!kind || *kind == Kind::Pipe) return Fail(GuestEbadf); + if (!kind || *kind == Kind::Pipe || *kind == Kind::Socket) return Fail(GuestEbadf); if (*kind != Kind::File && *kind != Kind::Directory) Unsupported("fcntl", "a record lock", *kind, fd); #ifdef _WIN32 return ProcessRecordLock(fd, *kind, current, lock, pointer, size); @@ -411,7 +415,7 @@ int RecordLock(int fd, int command, std::intptr_t argument) { int ReadAheadControl(int fd, int command, std::intptr_t argument) { const auto kind = Classify(fd, "fcntl"); - if (!kind || *kind == Kind::Pipe) return Fail(GuestEbadf); + if (!kind || *kind == Kind::Pipe || *kind == Kind::Socket) return Fail(GuestEbadf); if (*kind != Kind::File && *kind != Kind::ArchivedFile) return Fail(GuestEnotty); std::lock_guard lock(g_mutex); auto& status = *Materialize(fd).status; @@ -508,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); @@ -524,7 +537,7 @@ int Ioctl(int fd, std::uint64_t request, void* argument) { } if (command == ReadableRequest) { if (*kind == Kind::RandomDevice) return Fail(GuestEnotty); - if (*kind != Kind::File && *kind != Kind::Pipe && *kind != Kind::ArchivedFile) Unsupported("ioctl", "FIONREAD", *kind, fd); + if (*kind != Kind::File && *kind != Kind::Pipe && *kind != Kind::Socket && *kind != Kind::ArchivedFile) Unsupported("ioctl", "FIONREAD", *kind, fd); const auto archived = File::ArchivedFileRemaining(fd); const int available = archived ? static_cast(std::min(*archived, std::numeric_limits::max())) : Readable(fd, *kind, "ioctl"); if (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..7727bb9000 100644 --- a/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp +++ b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp @@ -153,12 +153,13 @@ std::optional SeekDirectoryDescriptor(int fd, std::int64_t offset, return target; } -int ReadDirectoryDescriptor(int fd, char* buf, int nbytes) { +int ReadDirectoryDescriptor(int fd, char* buf, int nbytes, std::int64_t* base) { const auto found = Find(fd); if (!found) return SCE_KERNEL_ERROR_ENOTDIR; auto& state = *found; std::lock_guard lock(state.mutex); Load(state, fd); + if (base != nullptr) *base = static_cast(state.cursor); std::size_t used = 0; while (state.cursor < state.entries.size()) { const auto& [name, type] = state.entries[state.cursor]; @@ -227,3 +228,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/NativeStat.cpp b/core/libs/prx/libkernel/File/src/NativeStat.cpp index 77e96bc6b9..75e7d82619 100644 --- a/core/libs/prx/libkernel/File/src/NativeStat.cpp +++ b/core/libs/prx/libkernel/File/src/NativeStat.cpp @@ -30,12 +30,11 @@ static int ReadHandleInfo(HANDLE handle, NativeStat* st) { st->hasIdentity = GetFileInformationByHandle(handle, &st->identity) != 0; return 0; } -static std::uint32_t FoldFileIndex(const BY_HANDLE_FILE_INFORMATION& identity) { - const std::uint32_t folded = identity.nFileIndexLow ^ (identity.nFileIndexHigh * 0x9E3779B1u); - return folded == 0 ? 1 : folded; -} static int DoStat(const std::filesystem::path& p, NativeStat* st) { - if (_wstat64(p.c_str(), st) != 0) return -1; + if (_wstat64(p.c_str(), st) != 0) { + if (_doserrno == ERROR_CANT_RESOLVE_FILENAME) File::WindowsFileTime::Failure(_doserrno); + return -1; + } const auto handle = CreateFileW(p.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 File::WindowsFileTime::Failure(GetLastError()); @@ -112,7 +111,7 @@ static void CopyNativeStat(const NativeStat& st, FileStat* sb) { } if (st.hasIdentity) { sb->st_dev = st.identity.dwVolumeSerialNumber; - sb->st_ino = FoldFileIndex(st.identity); + sb->st_ino = WindowsFileNumber_nid_no_patch(st.identity); sb->st_nlink = static_cast(std::min(st.identity.nNumberOfLinks, 0xffff)); } #else @@ -145,11 +144,16 @@ static void CopyNativeStat(const NativeStat& st, FileStat* sb) { namespace File { void FillFileStat(const std::filesystem::path& nativePath, FileStat* sb) { - NativeStat st{}; - if (DoStat(nativePath, &st) != 0) { + if (!TryFillFileStat(nativePath, sb)) { throw std::runtime_error(std::string("FillFileStat: stat failed for ") + nativePath.string()); } +} + +bool TryFillFileStat(const std::filesystem::path& nativePath, FileStat* sb) { + NativeStat st{}; + if (DoStat(nativePath, &st) != 0) return false; CopyNativeStat(st, sb); + return true; } void FillFileStat(int nativeDescriptor, FileStat* sb) { diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index 2c49e18af8..7743c86733 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; @@ -166,13 +174,19 @@ static int MapFlags(int sceFlags) { extern "C" int* APS5_VABI __error_nid_postfix(); extern "C" int APS5_VABI fcntl_nid_postfix(int descriptor, int command, ...); -static constexpr int GuestSetFlags = 4; static int SceErrorFromErrno(int error) { const int guest = error > 0 && error <= 34 ? error : FilesystemError(std::error_code(error, std::generic_category())); return static_cast(0x80020000u | static_cast(guest)); } +static int SceStatErrorFromErrno(int error) { +#ifdef ELOOP + if (error == ELOOP) return SCE_KERNEL_ERROR_ELOOP; +#endif + return SceErrorFromErrno(error); +} + static int SceIoErrorFromErrno(int error) { int guest = SceErrorFromErrno(error) & 0xffff; if (error == EAGAIN) guest = 35; @@ -206,10 +220,18 @@ 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 if (fd < 0 && (errno != ENOENT || !native.has_filename())) { + const int openError = errno; auto directory = native; while (!directory.has_filename() && directory.has_relative_path()) directory = directory.parent_path(); std::error_code error; @@ -217,11 +239,20 @@ static int OpenNative(const std::filesystem::path& native, int flags, std::uint1 if ((flags & (SCE_KERNEL_O_CREAT | SCE_KERNEL_O_EXCL)) == (SCE_KERNEL_O_CREAT | SCE_KERNEL_O_EXCL)) errno = EEXIST; else if ((flags & SCE_KERNEL_O_ACCMODE) != SCE_KERNEL_O_RDONLY || (flags & (SCE_KERNEL_O_CREAT | SCE_KERNEL_O_TRUNC))) errno = EISDIR; else fd = File::OpenDirectoryDescriptor(directory); + } else { + errno = openError; } } +#endif +#ifdef _WIN32 + if (fd < 0 && (errno == ENOENT || errno == ENOTDIR)) { + std::error_code metadataError; + const auto metadataPath = NativeMetadataPath(native, metadataError); + if (!metadataError && NativeSymbolicLinkLoop(metadataPath)) return SCE_KERNEL_ERROR_ELOOP; + } #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); @@ -232,6 +263,7 @@ static int OpenNative(const std::filesystem::path& native, int flags, std::uint1 extern "C" { int APS5_VABI sceKernelOpen(const char* path, int flags, std::uint16_t mode) { + if (File::InvalidOpenFlags(flags)) return SCE_KERNEL_ERROR_EINVAL; if (path != nullptr && *path == '\0') return SCE_KERNEL_ERROR_ENOENT; APS5_LOG_OUT("path=%s flags=0x%X nativeFlags=0x%X mode=0%o", path, flags, MapFlags(flags), mode); if (path != nullptr && File::IsRandomDevicePath(path)) { @@ -257,6 +289,7 @@ int APS5_VABI sceKernelOpen(const char* path, int flags, std::uint16_t mode) { } int OpenInDirectory_nid_no_patch(int directory, const char* path, int flags, std::uint16_t mode) { + if (File::InvalidOpenFlags(flags)) return SCE_KERNEL_ERROR_EINVAL; int error = 0; const char* base = GetDirectoryDescriptorPath_nid_no_patch(directory, &error); if (base == nullptr) return SceErrorFromErrno(error); @@ -338,16 +371,12 @@ std::int64_t APS5_VABI sceKernelLseek(int d, std::int64_t offset, int whence) { constexpr int GuestEfault = 14; -int APS5_VABI sceKernelStat(const char* path, FileStat* sb) { - if (path == nullptr) return SceErrorFromErrno(GuestEfault); - if (*path == '\0') return SceErrorFromErrno(2); - std::filesystem::path native; - if (const int error = ResolvePathError_nid_no_patch(path, native, true, true)) return SceKernelError(error); +static int StatNative(const std::filesystem::path& native, bool directory, FileStat* sb) { std::error_code error; constexpr int GuestEnotdir = 20; FileStat archived{}; if (File::FillArchivedStat(native, &archived)) { - if (std::string_view(path).back() == '/' && (archived.st_mode & 0170000) != 0040000) return SceErrorFromErrno(GuestEnotdir); + if (directory && (archived.st_mode & 0170000) != 0040000) return SceErrorFromErrno(GuestEnotdir); if (sb == nullptr) return SceErrorFromErrno(GuestEfault); *sb = archived; return 0; @@ -361,12 +390,33 @@ 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); + if (directory && !std::filesystem::is_directory(status)) return SceErrorFromErrno(GuestEnotdir); if (sb == nullptr) return SceErrorFromErrno(GuestEfault); - File::FillFileStat(metadataPath, sb); + if (!File::TryFillFileStat(metadataPath, sb)) return SceStatErrorFromErrno(errno); + return 0; +} + +int APS5_VABI sceKernelStat(const char* path, FileStat* sb) { + if (path == nullptr) return SceErrorFromErrno(GuestEfault); + if (*path == '\0') return SceErrorFromErrno(2); + std::filesystem::path native; + if (const int error = ResolvePathError_nid_no_patch(path, native, true, true)) return SceKernelError(error); + return StatNative(native, std::string_view(path).back() == '/', sb); +} + +int StatInDirectory_nid_no_patch(int directory, const char* path, FileStat* sb, bool follow) { + int error = 0; + const char* base = GetDirectoryDescriptorPath_nid_no_patch(directory, &error); + if (base == nullptr) return SceErrorFromErrno(error); + const auto native = std::filesystem::path(reinterpret_cast(base)) / std::filesystem::path(reinterpret_cast(path)); + if (follow) return StatNative(native, std::string_view(path).back() == '/', sb); + if (sb == nullptr) return SceErrorFromErrno(GuestEfault); + if (!File::FillLinkStat(native, sb)) return SceErrorFromErrno(errno); return 0; } @@ -379,7 +429,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; @@ -405,7 +455,8 @@ int APS5_VABI sceKernelFcntl(int d, int command, ...) { } int result; if (GuestSockets::IsOpen(d)) { - if (command == GuestSetFlags) result = fcntl_nid_postfix(d, command, static_cast(argument)); + if (type == File::FcntlArgument::Int) result = fcntl_nid_postfix(d, command, static_cast(argument)); + else if (type == File::FcntlArgument::Pointer) result = fcntl_nid_postfix(d, command, reinterpret_cast(argument)); else result = fcntl_nid_postfix(d, command); } else { result = File::Fcntl(d, command, argument); diff --git a/core/libs/prx/libkernel/File/src/OpenFileLimit.cpp b/core/libs/prx/libkernel/File/src/OpenFileLimit.cpp index 44498f642f..a5af366a2e 100644 --- a/core/libs/prx/libkernel/File/src/OpenFileLimit.cpp +++ b/core/libs/prx/libkernel/File/src/OpenFileLimit.cpp @@ -1,8 +1,11 @@ -#if defined(__linux__) +#if defined(__linux__) || defined(__APPLE__) #include #include #include +#ifdef __APPLE__ +#include +#endif namespace { @@ -10,8 +13,16 @@ struct OpenFileLimit { OpenFileLimit() { rlimit limit{}; if (getrlimit(RLIMIT_NOFILE, &limit) != 0) throw std::system_error(errno, std::generic_category(), "read open file limit"); - if (limit.rlim_cur >= limit.rlim_max) return; - limit.rlim_cur = limit.rlim_max; + rlim_t usable = limit.rlim_max; +#ifdef __APPLE__ + int perProcess = 0; + std::size_t size = sizeof(perProcess); + if (sysctlbyname("kern.maxfilesperproc", &perProcess, &size, nullptr, 0) != 0) throw std::system_error(errno, std::generic_category(), "read kern.maxfilesperproc"); + if (perProcess > 0 && static_cast(perProcess) < usable) usable = static_cast(perProcess); +#endif + if (limit.rlim_cur == usable && limit.rlim_max == usable) return; + limit.rlim_cur = usable; + limit.rlim_max = usable; if (setrlimit(RLIMIT_NOFILE, &limit) != 0) throw std::system_error(errno, std::generic_category(), "raise open file limit"); } } openFileLimit; diff --git a/core/libs/prx/libkernel/File/src/SandboxCertificates.cpp b/core/libs/prx/libkernel/File/src/SandboxCertificates.cpp index d41befaec6..a5af2341a3 100644 --- a/core/libs/prx/libkernel/File/src/SandboxCertificates.cpp +++ b/core/libs/prx/libkernel/File/src/SandboxCertificates.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #ifdef _WIN32 @@ -24,6 +25,34 @@ extern "C" const char* APS5_VABI sceKernelGetFsSandboxRandomWord(); namespace { constexpr char CertificateBegin[] = "-----BEGIN CERTIFICATE-----"; +constexpr char CertificateEnd[] = "-----END CERTIFICATE-----"; + +std::string CertificateBlocksOnly(const std::string& bundle) { + const std::string_view begin(CertificateBegin); + const std::string_view end(CertificateEnd); + std::string certificates; + std::size_t certificateStart = 0; + bool inCertificate = false; + for (std::size_t lineStart = 0; lineStart < bundle.size();) { + const auto newline = bundle.find('\n', lineStart); + const auto lineEnd = newline == std::string::npos ? bundle.size() : newline; + auto line = std::string_view(bundle).substr(lineStart, lineEnd - lineStart); + if (!line.empty() && line.back() == '\r') line.remove_suffix(1); + + if (line == begin) { + certificateStart = lineStart; + inCertificate = true; + } else if (line == end && inCertificate) { + const auto blockEnd = newline == std::string::npos ? lineEnd : newline + 1; + certificates.append(bundle, certificateStart, blockEnd - certificateStart); + inCertificate = false; + } + + if (newline == std::string::npos) break; + lineStart = newline + 1; + } + return certificates; +} std::optional ReadFile(const std::filesystem::path& path) { std::ifstream file(path, std::ios::binary); @@ -55,8 +84,8 @@ unsigned long ProcessId() { return static_cast(getpid()); } #endif std::filesystem::path WriteCertificateList(const std::filesystem::path& root) { - const auto bundle = HostCertificates(); - if (bundle.find(CertificateBegin) == std::string::npos) return {}; + const auto bundle = CertificateBlocksOnly(HostCertificates()); + if (bundle.empty()) return {}; const auto directory = root / "cert_cache"; std::filesystem::create_directories(directory); const auto target = directory / "CA_LIST.cer"; diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 5cce6b2fe5..2146bbe2d6 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" @@ -37,6 +38,10 @@ #include #include #include +#ifdef __APPLE__ +#include +#include +#endif struct KernelIovec { void* base; @@ -55,6 +60,7 @@ static constexpr std::uint64_t KERNEL_MNT_NOSUID = 0x8; #ifdef _WIN32 #include #include +#include #include #include #include @@ -136,7 +142,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); @@ -151,6 +167,8 @@ static int SetTimes(HANDLE handle, const KernelTimeval* times) { GetSystemTimePreciseAsFileTime(&access); modified = access; } else if (!File::WindowsFileTime::Encode(times[0], access) || !File::WindowsFileTime::Encode(times[1], modified)) { + const FILETIME unchanged{0xFFFFFFFF, 0xFFFFFFFF}; + SetFileTime(handle, nullptr, &unchanged, &unchanged); errno = EINVAL; return -1; } @@ -186,6 +204,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; } @@ -432,6 +452,11 @@ static int SceErrorFromErrno(int error) { return static_cast(0x80020000u | static_cast(guest)); } +static int PathLookupFailure(const std::error_code& error) { + const bool lookup = error == std::errc::filename_too_long || error == std::errc::too_many_symbolic_link_levels; + return lookup ? SceErrorFromErrno(error.default_error_condition().value()) : 0; +} + extern "C" int* APS5_VABI __error_nid_postfix(); static int PosixFailure(int error) { @@ -484,18 +509,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 +639,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; } @@ -668,22 +721,34 @@ int APS5_VABI _close_nid_postfix(int descriptor) { namespace { std::atomic g_hostStandardError{-1}; +std::atomic g_hostStandardOutput{-1}; -void PreserveHostStandardError() { - if (g_hostStandardError.load() >= 0) return; +void PreserveHostStream(int standard, std::atomic& slot) { + if (slot.load() >= 0) return; #ifdef _WIN32 - const int saved = WithoutParameterHandler([] { return ::_dup(2); }); + const int saved = WithoutParameterHandler([standard] { return ::_dup(standard); }); #else - const int saved = KeepHostStandardError_nid_no_patch(); + const int saved = standard == 1 ? KeepHostStandardOutput_nid_no_patch() : KeepHostStandardError_nid_no_patch(); #endif - if (saved < 0) throw std::runtime_error("dup2: cannot keep the host standard error"); + if (saved < 0) throw std::runtime_error(standard == 1 ? "dup2: cannot keep the host standard output" : "dup2: cannot keep the host standard error"); int expected = -1; - if (!g_hostStandardError.compare_exchange_strong(expected, saved)) { + if (!slot.compare_exchange_strong(expected, saved)) { #ifdef _WIN32 _close(saved); #endif + return; } +#ifdef _WIN32 + if (!AdoptHostStandardStream_nid_no_patch(standard, saved)) { + slot.store(-1); + _close(saved); + throw std::runtime_error(standard == 1 ? "dup2: cannot keep the host standard output" : "dup2: cannot keep the host standard error"); + } +#endif } + +void PreserveHostStandardError() { PreserveHostStream(2, g_hostStandardError); } +void PreserveHostStandardOutput() { PreserveHostStream(1, g_hostStandardOutput); } } int File::HostStandardError() { @@ -729,6 +794,7 @@ int APS5_VABI dup2_nid_postfix(int from, int to) { #ifdef _WIN32 if (WithoutParameterHandler([from] { return ::_get_osfhandle(from) == -1 ? -1 : 0; }) != 0) return PosixFailure(GUEST_EBADF); if (from == to) return to; + if (to == 1) PreserveHostStandardOutput(); if (to == 2) PreserveHostStandardError(); auto* const standard = HostStandardHandle_nid_no_patch(to); const int duplicated = WithoutParameterHandler([from, to] { return ::_dup2(from, to); }); @@ -749,6 +815,7 @@ int APS5_VABI dup2_nid_postfix(int from, int to) { if (::fcntl(from, F_GETFL) < 0) return PosixFailure(GUEST_EBADF); return to; } + if (to == 1) PreserveHostStandardOutput(); if (to == 2) PreserveHostStandardError(); const int result = ::dup2(from, to); if (result < 0) return PosixFailure(SceErrorFromErrno(errno) & 0xffff); @@ -765,6 +832,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) { @@ -837,6 +913,7 @@ int APS5_VABI sceKernelFtruncate(int d, int64_t length) { } int64_t APS5_VABI lseek_nid_postfix(int d, int64_t offset, int whence) { + if (!GuestSockets::IsOpen(d) && !DescriptorIsOpen_nid_no_patch(d)) return PosixFailure(GUEST_EBADF); if (whence < 0 || whence > 2) return PosixFailure(GUEST_EINVAL); return PosixResult(sceKernelLseek(d, offset, whence)); } @@ -855,6 +932,7 @@ int APS5_VABI mkdir_nid_postfix(const char* path, uint16_t mode) { } int APS5_VABI open_nid_postfix(const char* path, int flags, int mode) { + if (File::InvalidOpenFlags(flags)) return PosixFailure(GUEST_EINVAL); if (const int error = PathError(path)) return PosixFailure(error); return PosixResult(sceKernelOpen(path, flags, static_cast(mode))); } @@ -949,7 +1027,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; } @@ -1028,8 +1106,7 @@ int APS5_VABI sceKernelGetdirentries(int fd, char* buf, int nbytes, int64_t* bas const GuestArena::HostWrite destination(buf, static_cast(nbytes)); const GuestArena::HostWrite baseDestination(basep, basep == nullptr ? 0 : sizeof(*basep)); if (!destination.Open() || !baseDestination.Open()) return SceErrorFromErrno(GUEST_EFAULT); - if (basep != nullptr) *basep = 0; - return File::ReadDirectoryDescriptor(fd, buf, nbytes); + return File::ReadDirectoryDescriptor(fd, buf, nbytes, basep); } int APS5_VABI sceKernelGetdents(int fd, char* buf, int nbytes) { @@ -1109,6 +1186,7 @@ int APS5_VABI sceKernelMkdir(const char* path, uint16_t mode) { while (!native.has_filename() && native.has_relative_path()) native = native.parent_path(); std::error_code error; if (std::filesystem::exists(native, error)) return SceErrorFromErrno(GUEST_EEXIST); + if (const int failure = PathLookupFailure(error)) return failure; if (!std::filesystem::is_directory(WithoutTrailingSeparator(native).parent_path(), error)) { return SceErrorFromErrno(HasNonDirectoryPrefix(native) ? GUEST_ENOTDIR : GUEST_ENOENT); } @@ -1117,9 +1195,30 @@ int APS5_VABI sceKernelMkdir(const char* path, uint16_t mode) { return 0; } -static int CheckIovecs(const KernelIovec* iov, int iovcnt) { +static int CopyIovecs(const KernelIovec* iov, int iovcnt, std::vector& vectors) { if (iovcnt < 0 || iovcnt > KERNEL_IOV_MAX) return SceErrorFromErrno(GUEST_EINVAL); - if (iov == nullptr && iovcnt != 0) return SceErrorFromErrno(GUEST_EFAULT); + if (iovcnt == 0) return 0; + const auto bytes = static_cast(iovcnt) * sizeof(KernelIovec); + const auto address = reinterpret_cast(iov); + if (address == 0 || bytes > UINTPTR_MAX - address) return SceErrorFromErrno(GUEST_EFAULT); + GuestArena::GuestArenaPrepareHostRead_nid_no_patch(iov, bytes); + vectors.resize(static_cast(iovcnt)); +#ifdef _WIN32 + SIZE_T copied = 0; + if (!ReadProcessMemory(GetCurrentProcess(), iov, vectors.data(), bytes, &copied) || copied != bytes) return SceErrorFromErrno(GUEST_EFAULT); +#elif defined(__linux__) + iovec local{vectors.data(), bytes}; + iovec remote{const_cast(iov), bytes}; + const auto copied = ::process_vm_readv(::getpid(), &local, 1, &remote, 1, 0); + if (copied < 0 && errno != EFAULT) throw std::runtime_error("iovec: process_vm_readv failed with errno " + std::to_string(errno)); + if (copied < 0 || static_cast(copied) != bytes) return SceErrorFromErrno(GUEST_EFAULT); +#elif defined(__APPLE__) + mach_vm_size_t copied = 0; + if (::mach_vm_read_overwrite(::mach_task_self(), reinterpret_cast(iov), bytes, + reinterpret_cast(vectors.data()), &copied) != KERN_SUCCESS || copied != bytes) return SceErrorFromErrno(GUEST_EFAULT); +#else + throw std::runtime_error("iovec: checked guest copies are not implemented on this host"); +#endif return 0; } @@ -1168,8 +1267,10 @@ int64_t APS5_VABI sceKernelPwrite(int d, const void* buf, size_t nbytes, int64_t return result < 0 ? SceErrorFromErrno(errno) : result; } -static std::int64_t TransferIovecs(int d, const KernelIovec* iov, int iovcnt, const std::int64_t* offset, bool write) { - if (const int error = CheckIovecs(iov, iovcnt)) return error; +static std::int64_t TransferIovecs(int d, const KernelIovec* guestIov, int iovcnt, const std::int64_t* offset, bool write) { + std::vector vectors; + if (const int error = CopyIovecs(guestIov, iovcnt, vectors)) return error; + const auto* iov = vectors.data(); if (offset != nullptr && *offset < 0) return SceErrorFromErrno(GUEST_EINVAL); std::size_t total = 0; for (int i = 0; i < iovcnt; ++i) { @@ -1265,8 +1366,10 @@ int64_t APS5_VABI sceKernelPwrite(int d, const void* buf, size_t nbytes, int64_t return result < 0 ? SceErrorFromErrno(errno) : result; } -int64_t APS5_VABI sceKernelReadv(int d, const KernelIovec* iov, int iovcnt) { - if (const int error = CheckIovecs(iov, iovcnt)) return error; +int64_t APS5_VABI sceKernelReadv(int d, const KernelIovec* guestIov, int iovcnt) { + std::vector vectors; + if (const int error = CopyIovecs(guestIov, iovcnt, vectors)) return error; + const auto* iov = vectors.data(); std::deque destinations; if (!OpenIovecs(iov, iovcnt, destinations)) return SceErrorFromErrno(GUEST_EFAULT); if (GuestSockets::IsOpen(d)) { @@ -1279,8 +1382,10 @@ int64_t APS5_VABI sceKernelReadv(int d, const KernelIovec* iov, int iovcnt) { return result < 0 ? SceErrorFromErrno(errno) : result; } -int64_t APS5_VABI sceKernelWritev(int d, const KernelIovec* iov, int iovcnt) { - if (const int error = CheckIovecs(iov, iovcnt)) return error; +int64_t APS5_VABI sceKernelWritev(int d, const KernelIovec* guestIov, int iovcnt) { + std::vector vectors; + if (const int error = CopyIovecs(guestIov, iovcnt, vectors)) return error; + const auto* iov = vectors.data(); if (GuestSockets::IsOpen(d)) { const auto result = GuestSockets::Writev(d, reinterpret_cast(iov), iovcnt); return result < 0 ? SceKernelError(*__error_nid_postfix()) : result; @@ -1292,8 +1397,10 @@ int64_t APS5_VABI sceKernelWritev(int d, const KernelIovec* iov, int iovcnt) { return result < 0 ? SceErrorFromErrno(errno) : result; } -int64_t APS5_VABI sceKernelPreadv(int d, const KernelIovec* iov, int iovcnt, int64_t offset) { - if (const int error = CheckIovecs(iov, iovcnt)) return error; +int64_t APS5_VABI sceKernelPreadv(int d, const KernelIovec* guestIov, int iovcnt, int64_t offset) { + std::vector vectors; + if (const int error = CopyIovecs(guestIov, iovcnt, vectors)) return error; + const auto* iov = vectors.data(); if (offset < 0) return SceErrorFromErrno(GUEST_EINVAL); if (const auto error = PositionedGuestSocketError(d); error != 0) return error; std::deque destinations; @@ -1304,8 +1411,10 @@ int64_t APS5_VABI sceKernelPreadv(int d, const KernelIovec* iov, int iovcnt, int return result < 0 ? SceErrorFromErrno(errno) : result; } -int64_t APS5_VABI sceKernelPwritev(int d, const KernelIovec* iov, int iovcnt, int64_t offset) { - if (const int error = CheckIovecs(iov, iovcnt)) return error; +int64_t APS5_VABI sceKernelPwritev(int d, const KernelIovec* guestIov, int iovcnt, int64_t offset) { + std::vector vectors; + if (const int error = CopyIovecs(guestIov, iovcnt, vectors)) return error; + const auto* iov = vectors.data(); if (offset < 0) return SceErrorFromErrno(GUEST_EINVAL); if (const auto error = PositionedGuestSocketError(d); error != 0) return error; PrepareWriteIovecs(iov, iovcnt); @@ -1328,9 +1437,11 @@ int64_t APS5_VABI pwritev_nid_postfix(int d, const KernelIovec* iov, int iovcnt, } int APS5_VABI sceKernelRename(const char* from, const char* to) { - if (!from || !to) return SceErrorFromErrno(GUEST_EFAULT); + if (!from) return SceErrorFromErrno(GUEST_EFAULT); const int result = RenameGuestPath_nid_no_patch(from, to); return result == 0 ? 0 : SceKernelError(errno); + if (const int dot = RenameDotError_nid_no_patch(from)) return SceKernelError(dot); + if (const int dot = RenameDotError_nid_no_patch(to)) return SceKernelError(dot); } int APS5_VABI sceKernelRmdir(const char* path) { @@ -1340,6 +1451,7 @@ int APS5_VABI sceKernelRmdir(const char* path) { if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); std::error_code error; if (std::filesystem::is_symlink(native, error)) return SceErrorFromErrno(GUEST_ENOTDIR); + if (const int failure = PathLookupFailure(error)) return failure; if (!std::filesystem::is_directory(native, error)) { const bool blockedByFile = HasNonDirectoryPrefix(native) || std::filesystem::exists(WithoutTrailingSeparator(native), error); return SceErrorFromErrno(blockedByFile ? GUEST_ENOTDIR : GUEST_ENOENT); @@ -1380,15 +1492,20 @@ 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 (const int failure = PathLookupFailure(error)) return failure; + 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); @@ -1396,36 +1513,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/File/src/Terminal.cpp b/core/libs/prx/libkernel/File/src/Terminal.cpp index 648c63eb7c..25f8a85526 100644 --- a/core/libs/prx/libkernel/File/src/Terminal.cpp +++ b/core/libs/prx/libkernel/File/src/Terminal.cpp @@ -1,6 +1,8 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libkernel/Socket/include/SocketRuntime.hpp" +#include #include +#include #include #ifdef _WIN32 #include @@ -17,6 +19,15 @@ extern "C" _invalid_parameter_handler _set_thread_local_invalid_parameter_handle #endif namespace { +struct GuestTermios { + std::uint32_t flags[4]; + std::uint8_t controlCharacters[20]; + std::uint32_t inputSpeed; + std::uint32_t outputSpeed; +}; +static_assert(sizeof(GuestTermios) == 44 && alignof(GuestTermios) == 4); +static_assert(offsetof(GuestTermios, inputSpeed) == 36 && offsetof(GuestTermios, outputSpeed) == 40); + constexpr int BadDescriptor = 9; constexpr int BadAddress = 14; constexpr int InvalidArgument = 22; @@ -73,6 +84,14 @@ int ProcessGroupError(int descriptor, const char* pipeMessage) { } extern "C" { +int APS5_VABI cfsetspeed_nid_postfix(void* attributes, std::uint32_t speed) { + if (attributes == nullptr) throw std::invalid_argument("cfsetspeed: attributes is null"); + auto* bytes = static_cast(attributes); + std::memcpy(bytes + offsetof(GuestTermios, inputSpeed), &speed, sizeof(speed)); + std::memcpy(bytes + offsetof(GuestTermios, outputSpeed), &speed, sizeof(speed)); + return 0; +} + int APS5_VABI tcgetattr_nid_postfix(int descriptor, void* attributes) { static_cast(attributes); return Fail(TerminalError(descriptor)); diff --git a/core/libs/prx/libkernel/Module/src/Module.cpp b/core/libs/prx/libkernel/Module/src/Module.cpp index 6e06be8deb..9e71a3183c 100644 --- a/core/libs/prx/libkernel/Module/src/Module.cpp +++ b/core/libs/prx/libkernel/Module/src/Module.cpp @@ -1,8 +1,10 @@ #include #include #include +#include #include #include +#include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -153,10 +155,22 @@ extern "C" { void* APS5_VABI dlopen_nid_postfix(const char* path, int flags); void* APS5_VABI dlsym_nid_postfix(void* handle, const char* name); int APS5_VABI dlclose_nid_postfix(void* handle); +const char* APS5_VABI sceKernelGetFsSandboxRandomWord(); } namespace { constexpr int kRtldNow = 2; + +std::optional SystemLibraryPath(std::string_view path) { + const std::string sandbox = std::string("/") + sceKernelGetFsSandboxRandomWord() + "/common/lib/"; + for (const std::string_view prefix : {std::string_view("/system/common/lib/"), std::string_view(sandbox)}) { + if (!path.starts_with(prefix)) continue; + const auto name = path.substr(prefix.size()); + if (name.size() <= 5 || !name.ends_with(".sprx") || name.find('/') != std::string_view::npos) return std::nullopt; + return "libs/" + std::string(name.substr(0, name.size() - 5)) + ".prx"; + } + return std::nullopt; +} } extern "C" { @@ -281,7 +295,8 @@ KernelModule APS5_VABI sceKernelLoadStartModule(const char* module_file_name, si if (!module_file_name) return static_cast(SCE_KERNEL_ERROR_EFAULT); pendingModuleArgs = {args, argp}; pendingModuleInitResult = 0; - void* handle = dlopen_nid_postfix(module_file_name, kRtldNow); + const auto systemLibrary = SystemLibraryPath(module_file_name); + void* handle = dlopen_nid_postfix(systemLibrary ? systemLibrary->c_str() : module_file_name, kRtldNow); const int started = pendingModuleInitResult; pendingModuleArgs = {}; pendingModuleInitResult = 0; @@ -306,6 +321,17 @@ extern "C" { int APS5_VABI __elf_phdr_match_addr_nid_postfix(dl_phdr_info* phdrInfo, void* addr) { if (phdrInfo == nullptr) throw std::invalid_argument("__elf_phdr_match_addr: phdr_info is null"); const auto address = reinterpret_cast(addr); + + const auto* module = reinterpret_cast(phdrInfo); + if (module->st_size == sizeof(ModuleInfoEx)) { + for (std::uint32_t i = 0; i < module->segment_count && i < std::size(module->segments); ++i) { + const auto& segment = module->segments[i]; + if ((segment.prot & 4) == 0 || segment.size < sizeof(addr)) continue; + if (segment.address <= address && address - segment.address <= segment.size - sizeof(addr)) return 1; + } + return 0; + } + for (std::uint16_t i = 0; i < phdrInfo->dlpi_phnum; ++i) { const Elf64_Phdr& header = phdrInfo->dlpi_phdr[i]; if (header.p_type != PT_LOAD || (header.p_flags & PF_X) == 0) continue; diff --git a/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp b/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp index 1ea13b5de0..86fd01c134 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp @@ -38,6 +38,7 @@ int APS5_VABI pthread_attr_get_np_nid_postfix(Pthread thread, PthreadAttr* attr) p->stackAddress = thread->stackAddress; p->_detachstate = thread->_detached ? 1 : 0; p->_schedpriority = thread->priority.load(std::memory_order_relaxed); + p->_schedpolicy = thread->schedPolicy.load(std::memory_order_relaxed); p->_affinity = thread->affinity.load(std::memory_order_relaxed); p->_affinityExplicit = true; p->_scope = thread->scope; 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/Posix/Thread.cpp b/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp index c2e3d1aaee..3a0ed90496 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp @@ -9,7 +9,7 @@ #include "Common.hpp" #include "prx/libkernel/KernelErrors.hpp" #include -#include +#include #include #include @@ -27,6 +27,7 @@ int APS5_VABI scePthreadSetcanceltype(int type, int* old_type); void APS5_VABI scePthreadTestcancel(); int APS5_VABI scePthreadSetprio(Pthread thread, int prio); int APS5_VABI scePthreadGetprio(Pthread thread, int* prio); +void APS5_VABI scePthreadYield(); } static constexpr int GUEST_SCHED_FIFO = 1; @@ -194,6 +195,15 @@ int APS5_VABI pthread_getschedparam_nid_postfix(Pthread thread, int* policy, Ker return PosixThread::ToErrno(scePthreadGetschedparam(thread, policy, param)); } +int APS5_VABI pthread_getprio_nid_postfix(Pthread thread) { + const auto lock = LockLiveGuestThread(thread); + if (!lock.owns_lock()) { + *__error_nid_postfix() = PosixThread::GUEST_ESRCH; + return -1; + } + return thread->priority.load(std::memory_order_relaxed); +} + int APS5_VABI pthread_getaffinity_np_nid_postfix(Pthread thread, std::size_t cpusetSize, void* cpuset) { if (!thread) return PosixThread::GUEST_EINVAL; if (cpusetSize != GUEST_CPUSET_BYTES) return GUEST_ERANGE; @@ -225,6 +235,17 @@ int APS5_VABI pthread_join_nid_postfix(Pthread thread, void** value) { return PosixThread::ToErrno(scePthreadJoin(thread, value)); } +namespace { +struct RegisterPthreadOnLoad { + RegisterPthreadOnLoad() { + RegisterPthreadCreate_nid_no_patch(reinterpret_cast(pthread_create_nid_postfix)); + RegisterPthreadJoin_nid_no_patch(reinterpret_cast(pthread_join_nid_postfix)); + RegisterPthreadDetach_nid_no_patch(reinterpret_cast(pthread_detach_nid_postfix)); + } +}; +const RegisterPthreadOnLoad gRegisterPthread; +} + int APS5_VABI pthread_rename_np_nid_postfix(Pthread thread, const char* name) { if (thread && name && std::strlen(name) >= 32) return PosixThread::ToErrno(SCE_KERNEL_ERROR_ENAMETOOLONG); return PosixThread::ToErrno(scePthreadRename(thread, name)); @@ -266,7 +287,7 @@ int APS5_VABI pthread_getcpuclockid_nid_postfix(Pthread thread, int* clockId) { } int APS5_VABI sched_yield_nid_postfix(void) { - std::this_thread::yield(); + scePthreadYield(); return 0; } @@ -296,12 +317,20 @@ void APS5_VABI pthread_testcancel_nid_postfix(void) { } void APS5_VABI pthread_yield_nid_postfix(void) { - std::this_thread::yield(); + scePthreadYield(); } unsigned int APS5_VABI sleep_nid_postfix(unsigned int seconds) { ThreadCancel::Check(); - std::this_thread::sleep_for(std::chrono::seconds(seconds)); + { + const auto deadline = TimedWait::DeadlineNanos(static_cast(seconds) * 1000000ULL); + std::mutex mutex; + TimedWait::Condition condition; + std::unique_lock lock(mutex); + while (!ThreadCancel::Requested()) { + if (!ThreadCancel::Wait(condition, lock, deadline)) break; + } + } ThreadCancel::Check(); return 0; } 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 4102a9e69e..ce6ccac93c 100644 --- a/core/libs/prx/libkernel/Pthread/src/Attr.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Attr.cpp @@ -77,11 +77,13 @@ 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; (*attr)->_schedpriority = thread->priority.load(std::memory_order_relaxed); - (*attr)->_schedpolicy = SCHED_FIFO_PS5; + (*attr)->_schedpolicy = thread->schedPolicy.load(std::memory_order_relaxed); (*attr)->_inheritsched = 4; (*attr)->_affinity = thread->affinity.load(std::memory_order_relaxed); (*attr)->_affinityExplicit = true; @@ -108,12 +110,25 @@ int APS5_VABI scePthreadAttrGetguardsize(const PthreadAttr* attr, size_t* guard_ return SCE_OK; } +int APS5_VABI scePthreadAttrGetinheritsched(const PthreadAttr* attr, int* inheritSched) { + if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; + if (!inheritSched) throw std::runtime_error(std::string(__func__) + ": null output"); + *inheritSched = (*attr)->_inheritsched; + return SCE_OK; +} + int APS5_VABI scePthreadAttrGetschedparam(const PthreadAttr* attr, KernelSchedParam* param) { if (!attr || !*attr || !param) return SCE_KERNEL_ERROR_EINVAL; param->sched_priority = (*attr)->_schedpriority; return SCE_OK; } +int APS5_VABI scePthreadAttrGetschedpolicy(const PthreadAttr* attr, int* policy) { + if (!attr || !*attr || !policy) return SCE_KERNEL_ERROR_EINVAL; + *policy = (*attr)->_schedpolicy; + return SCE_OK; +} + int APS5_VABI scePthreadAttrGetsolosched(const PthreadAttr* attr, int* solosched) { if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; if (!solosched) throw std::runtime_error(std::string(__func__) + ": null output"); diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index 0afb764b67..cd2cab33c0 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -43,6 +43,7 @@ static constexpr std::size_t THREAD_NAME_CAPACITY = 32; #ifdef _WIN32 #include #include +#include #endif struct ThreadArgs { @@ -439,14 +440,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) { @@ -1029,7 +1033,28 @@ Pthread APS5_VABI scePthreadSelf() { } void APS5_VABI scePthreadYield() { +#ifdef _WIN32 + thread_local unsigned int spins = 0; + thread_local std::uint64_t lastTsc = 0; + const auto now = __rdtsc(); + if (now - lastTsc > 10000) { + spins = 0; + } + lastTsc = now; + if (SwitchToThread()) { + spins = 0; + return; + } + if (++spins < 512) { + _mm_pause(); + return; + } + spins = 0; + Sleep(1); + lastTsc = __rdtsc(); +#else std::this_thread::yield(); +#endif } int APS5_VABI scePthreadCancel(Pthread thread) { @@ -1041,7 +1066,7 @@ int APS5_VABI scePthreadCancel(Pthread thread) { if (!thread->cancelWait) return SCE_OK; thread->cancelWait->NotifyAll(); } - std::this_thread::yield(); + scePthreadYield(); } } 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/include/SocketPoll.hpp b/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp index 9bf3b90265..97e68db173 100644 --- a/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp +++ b/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp @@ -20,6 +20,10 @@ using Poller = int (*)(Entry* entries, int count, int timeoutMilliseconds); using Closer = bool (*)(int descriptor, int* error); using OptionSetter = bool (*)(int descriptor, int level, int option, const void* value, std::uint32_t length, int* error); using StatusControl = bool (*)(int descriptor, int command, int flags, int* result, int* error); +struct NativeOwner { + int (*transfer)(NativeOwner* owner, bool send, void* buffer, int length) = nullptr; +}; + using NativeLookup = bool (*)(int descriptor, std::shared_ptr* owner, std::uintptr_t* native); } diff --git a/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp b/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp index 5b22edcf5d..d59cfc4304 100644 --- a/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp +++ b/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp @@ -18,6 +18,7 @@ bool IsOpen(int descriptor); int Family(int descriptor); int Type(int descriptor); bool Listening(int descriptor); +std::intptr_t NativeDescriptor(int descriptor); std::int64_t Read(int descriptor, void* buffer, std::size_t length); std::int64_t Write(int descriptor, const void* buffer, std::size_t length); int Stat(int descriptor, FileStat* status); @@ -34,6 +35,7 @@ std::uintptr_t CurrentWaker(); void Wake(std::uintptr_t waker); bool WaitAny(const std::vector& interests, std::uint64_t deadlineNanos); bool PipeReadState(int descriptor, std::int64_t* data, bool* eof); +bool PipeWriteState(int descriptor, std::int64_t* data, bool* eof, bool* writable); std::int64_t Readv(int descriptor, const Iovec* iov, int iovcnt); std::int64_t Writev(int descriptor, const Iovec* iov, int iovcnt); } diff --git a/core/libs/prx/libkernel/Socket/src/AddressConversion.cpp b/core/libs/prx/libkernel/Socket/src/AddressConversion.cpp index 1ed1a2e20a..bbcb17e637 100644 --- a/core/libs/prx/libkernel/Socket/src/AddressConversion.cpp +++ b/core/libs/prx/libkernel/Socket/src/AddressConversion.cpp @@ -2,6 +2,7 @@ #include #include #include +#include extern "C" int* APS5_VABI __error_nid_postfix(); @@ -189,15 +190,17 @@ std::size_t V6ToText(const std::uint8_t* bytes, char* out) { extern "C" { int APS5_VABI __inet_pton_nid_postfix(int family, const char* text, void* destination) { if (family != kGuestInet && family != kGuestInet6) { *__error_nid_postfix() = 47; return -1; } - if (!text || !destination) { *__error_nid_postfix() = 14; return -1; } + if (!text) throw std::runtime_error("inet_pton: null text"); if (family == kGuestInet) { std::uint8_t quad[4]; if (!ParseQuad(text, text + std::strlen(text), quad)) return 0; + if (!destination) throw std::runtime_error("inet_pton: null destination"); std::memcpy(destination, quad, sizeof(quad)); return 1; } std::uint8_t address[16]; if (!TextToV6(text, address)) return 0; + if (!destination) throw std::runtime_error("inet_pton: null destination"); std::memcpy(destination, address, sizeof(address)); return 1; } @@ -205,13 +208,31 @@ int APS5_VABI __inet_pton_nid_postfix(int family, const char* text, void* destin const char* APS5_VABI __inet_ntop_nid_postfix(int family, const void* source, char* destination, std::uint32_t capacity) { if (family != kGuestInet && family != kGuestInet6) { *__error_nid_postfix() = 47; return nullptr; } - if (!source || !destination) { *__error_nid_postfix() = 14; return nullptr; } + if (!source) throw std::runtime_error("inet_ntop: null source"); char text[kMaxText + 1]; const auto* bytes = static_cast(source); const std::size_t length = family == kGuestInet ? FormatQuad(bytes, text) : V6ToText(bytes, text); text[length] = '\0'; if (length + 1 > capacity) { *__error_nid_postfix() = 28; return nullptr; } + if (!destination) throw std::runtime_error("inet_ntop: null destination"); std::memcpy(destination, text, length + 1); + if (family == kGuestInet6) std::memset(destination + length + 1, 0, capacity - length - 1); return destination; } + +std::uint32_t APS5_VABI htonl_nid_postfix(std::uint32_t value) { + return __builtin_bswap32(value); +} + +std::uint32_t APS5_VABI ntohl_nid_postfix(std::uint32_t value) { + return __builtin_bswap32(value); +} + +std::uint16_t APS5_VABI htons_nid_postfix(std::uint16_t value) { + return __builtin_bswap16(value); +} + +std::uint16_t APS5_VABI ntohs_nid_postfix(std::uint16_t value) { + return __builtin_bswap16(value); +} } 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 b4c71263e0..f9a28df4fd 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -18,8 +18,15 @@ #include #include #include +#ifdef __linux__ +#include +#elif defined(__APPLE__) +#include +#include +#endif #endif #include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/PreciseWait.hpp" #include "prx/libc/include/general/VabiMacros.hpp" @@ -51,6 +58,7 @@ #include extern "C" int* APS5_VABI __error_nid_postfix(); +extern "C" int APS5_VABI dup2_nid_postfix(int from, int to); namespace { #ifdef _WIN32 using NativeSocket = SOCKET; @@ -60,16 +68,12 @@ using NativeSocket = int; constexpr auto Invalid = -1; #endif int Fail(int error) { *__error_nid_postfix() = error; return -1; } +constexpr int GuestSocketDescriptorLimit = 1024; 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); @@ -106,7 +110,7 @@ int SendError(int type) { #endif return NativeError(); } -struct Socket { +struct Socket : KernelSocketPoll::NativeOwner { NativeSocket value; int family; int type; @@ -178,13 +182,35 @@ int ReserveNumber(NativeSocket) { ::CloseHandle(placeholder); return Fail(24); } + if (number >= GuestSocketDescriptorLimit) { + ::_close(number); + return Fail(24); + } return number; } -int CopyNumber(int number) { - const int copy = ::_dup(number); - return copy < 0 ? Fail(errno == EMFILE ? 24 : 9) : copy; +int CopyNumber(int number, int minimum = 0) { + if (minimum >= GuestSocketDescriptorLimit) return Fail(24); + std::vector below; + int copy = -1; + int error = 0; + try { + while ((copy = ::_dup(number)) >= 0 && copy < minimum) below.push_back(copy); + if (copy < 0) error = errno == EMFILE ? 24 : 9; + } catch (const std::bad_alloc&) { + if (copy >= 0) ::_close(copy); + copy = -1; + error = 12; + } + for (const int temporary : below) ::_close(temporary); + if (copy < 0) return Fail(error); + if (copy >= GuestSocketDescriptorLimit) { + ::_close(copy); + return Fail(24); + } + return copy; } int CopyNumberTo(int number, int target) { + if (target >= GuestSocketDescriptorLimit) return Fail(24); return ::_dup2(number, target) != 0 ? Fail(errno == EMFILE ? 24 : 9) : 0; } void ReleaseNumber(int number) { @@ -193,13 +219,25 @@ void ReleaseNumber(int number) { #else int ReserveNumber(NativeSocket native) { const int number = ::fcntl(native, F_DUPFD_CLOEXEC, 0); - return number < 0 ? Fail(NativeError()) : number; + if (number < 0) return Fail(NativeError()); + if (number >= GuestSocketDescriptorLimit) { + ::close(number); + return Fail(24); + } + return number; } -int CopyNumber(int number) { - const int copy = ::fcntl(number, F_DUPFD_CLOEXEC, 0); - return copy < 0 ? Fail(NativeError()) : copy; +int CopyNumber(int number, int minimum = 0) { + if (minimum >= GuestSocketDescriptorLimit) return Fail(24); + const int copy = ::fcntl(number, F_DUPFD_CLOEXEC, minimum); + if (copy < 0) return Fail(NativeError()); + if (copy >= GuestSocketDescriptorLimit) { + ::close(copy); + return Fail(24); + } + return copy; } int CopyNumberTo(int number, int target) { + if (target >= GuestSocketDescriptorLimit) return Fail(24); return ::dup2(number, target) < 0 ? Fail(NativeError()) : 0; } void ReleaseNumber(int number) { @@ -222,12 +260,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; @@ -253,9 +294,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; @@ -373,7 +413,7 @@ bool GuestSockets::WaitAny(const std::vector& interests, std::uint64_t #ifdef _WIN32 pipes = true; #else - entries.push_back({interest.descriptor, ReadMask, 0}); + entries.push_back({interest.descriptor, interest.write ? WriteMask : ReadMask, 0}); #endif continue; } @@ -407,6 +447,12 @@ bool GuestSockets::WaitAny(const std::vector& interests, std::uint64_t return true; } +std::intptr_t GuestSockets::NativeDescriptor(int descriptor) { + std::lock_guard lock(socketsMutex); + const auto found = sockets.find(descriptor); + return found == sockets.end() ? -1 : static_cast(found->second->value); +} + bool GuestSockets::IsOpen(int descriptor) { std::lock_guard lock(socketsMutex); return sockets.contains(descriptor); @@ -429,9 +475,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) { @@ -554,21 +598,30 @@ bool GuestSockets::PipeReady(int descriptor, short events, short& revents) { } #endif -int GuestSockets::Duplicate(int descriptor) { +namespace { +int DuplicateSocket_nid_no_patch(int descriptor, int minimum, bool closeOnExec) { + if (minimum < 0) return Fail(22); std::lock_guard lock(socketsMutex); const auto found = sockets.find(descriptor); if (found == sockets.end()) return Fail(9); - const int duplicate = CopyNumber(descriptor); + const int duplicate = CopyNumber(descriptor, minimum); if (duplicate < 0) return -1; try { sockets.insert_or_assign(duplicate, found->second); - closeOnExecDescriptors.erase(duplicate); + if (closeOnExec) closeOnExecDescriptors.insert(duplicate); + else closeOnExecDescriptors.erase(duplicate); } catch (const std::bad_alloc&) { + sockets.erase(duplicate); ReleaseNumber(duplicate); return Fail(12); } return duplicate; } +} + +int GuestSockets::Duplicate(int descriptor) { + return DuplicateSocket_nid_no_patch(descriptor, 0, false); +} int GuestSockets::DuplicateTo(int descriptor, int target) { std::shared_ptr replaced; @@ -619,7 +672,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); @@ -770,7 +832,11 @@ bool GuestSockets::Ready(int descriptor, bool write, std::int64_t* data, bool* e { std::lock_guard lock(socketsMutex); const auto found = sockets.find(descriptor); - if (found == sockets.end()) return !write && PipeReadState(descriptor, data, eof) && (*data > 0 || *eof); + if (found == sockets.end() && !write) return PipeReadState(descriptor, data, eof) && (*data > 0 || *eof); + if (found == sockets.end()) { + bool writable = false; + return PipeWriteState(descriptor, data, eof, &writable) && (writable || *eof); + } socket = found->second; } #ifdef _WIN32 @@ -824,6 +890,31 @@ bool GuestSockets::PipeReadState(int descriptor, std::int64_t* data, bool* eof) return true; } +bool GuestSockets::PipeWriteState(int descriptor, std::int64_t* data, bool* eof, bool* writable) { +#ifdef _WIN32 + ACCESS_MASK access = 0; + PipeLocalInformation pipe{}; + if (!QueryPipe(descriptor, "kevent", access, pipe) || !(access & FILE_WRITE_DATA)) return false; + *eof = pipe.state == PipeClosingState; + *writable = pipe.writeQuotaAvailable >= PipeAtomicBytes; + *data = *eof ? 0 : pipe.writeQuotaAvailable; +#elif defined(__linux__) + if (!IsPipe(descriptor, 0) || (::fcntl(descriptor, F_GETFL) & O_ACCMODE) == O_RDONLY) return false; + pollfd entry{descriptor, POLLOUT, 0}; + int queued = 0; + const int capacity = ::fcntl(descriptor, F_GETPIPE_SZ); + if (::poll(&entry, 1, 0) < 0 || ::ioctl(descriptor, FIONREAD, &queued) != 0 || capacity < 0) + throw std::runtime_error("kevent: querying pipe descriptor " + std::to_string(descriptor) + " failed"); + *eof = (entry.revents & POLLERR) != 0; + *writable = (entry.revents & POLLOUT) != 0; + *data = *eof ? 0 : std::max(0, capacity - queued); +#else + if (!IsPipe(descriptor, 0) || (::fcntl(descriptor, F_GETFL) & O_ACCMODE) == O_RDONLY) return false; + NotImplemented_nid_no_patch("kevent: EVFILT_WRITE on a pipe on macOS"); +#endif + return true; +} + int GuestSockets::Poll(KernelSocketPoll::Entry* entries, int count, int timeoutMilliseconds) { std::vector> held; std::vector native; @@ -872,7 +963,7 @@ int GuestSockets::Poll(KernelSocketPoll::Entry* entries, int count, int timeoutM extern "C" bool KernelLookupSocketNative_nid_no_patch(int descriptor, std::shared_ptr* owner, std::uintptr_t* native) { if (auto socket = Find(descriptor)) { *native = static_cast(socket->value); - *owner = std::move(socket); + *owner = std::static_pointer_cast(std::move(socket)); return true; } return KernelFindSocketNative_nid_no_patch(descriptor, owner, native); @@ -880,24 +971,24 @@ extern "C" bool KernelLookupSocketNative_nid_no_patch(int descriptor, std::share extern "C" { int APS5_VABI fcntl_nid_postfix(int descriptor, int command, ...) { - if (!GuestSockets::IsOpen(descriptor)) { - const auto type = File::FcntlArgumentOf(command); - std::intptr_t argument = 0; - if (type != File::FcntlArgument::None) { + const auto type = File::FcntlArgumentOf(command); + std::intptr_t argument = 0; + if (type != File::FcntlArgument::None) { #ifdef _WIN32 - __builtin_sysv_va_list arguments; - __builtin_sysv_va_start(arguments, command); - argument = type == File::FcntlArgument::Int ? __builtin_va_arg(arguments, int) - : reinterpret_cast(__builtin_va_arg(arguments, void*)); - __builtin_sysv_va_end(arguments); + __builtin_sysv_va_list arguments; + __builtin_sysv_va_start(arguments, command); + argument = type == File::FcntlArgument::Int ? __builtin_va_arg(arguments, int) + : reinterpret_cast(__builtin_va_arg(arguments, void*)); + __builtin_sysv_va_end(arguments); #else - std::va_list arguments; - va_start(arguments, command); - argument = type == File::FcntlArgument::Int ? va_arg(arguments, int) - : reinterpret_cast(va_arg(arguments, void*)); - va_end(arguments); + std::va_list arguments; + va_start(arguments, command); + argument = type == File::FcntlArgument::Int ? va_arg(arguments, int) + : reinterpret_cast(va_arg(arguments, void*)); + va_end(arguments); #endif - } + } + if (!GuestSockets::IsOpen(descriptor)) { int result = 0; int netError = 0; if (KernelControlSocketStatus_nid_no_patch(descriptor, command, static_cast(argument), &result, &netError)) { @@ -905,6 +996,12 @@ int APS5_VABI fcntl_nid_postfix(int descriptor, int command, ...) { } return File::Fcntl(descriptor, command, argument); } + if (command == 0 || command == 17) return DuplicateSocket_nid_no_patch(descriptor, static_cast(argument), command == 17); + if (command == 10 || command == 18) { + const int duplicate = dup2_nid_postfix(descriptor, static_cast(argument)); + if (duplicate >= 0 && command == 18) fcntl_nid_postfix(duplicate, 2, 1); + return duplicate; + } const auto socket = Lookup(descriptor); if (!socket) return -1; std::lock_guard lock(socket->modeMutex); @@ -913,17 +1010,7 @@ int APS5_VABI fcntl_nid_postfix(int descriptor, int command, ...) { return closeOnExecDescriptors.count(descriptor) != 0 ? 1 : 0; } if (command == 2) { -#ifdef _WIN32 - __builtin_sysv_va_list arguments; - __builtin_sysv_va_start(arguments, command); - const int flags = __builtin_va_arg(arguments, int); - __builtin_sysv_va_end(arguments); -#else - std::va_list arguments; - va_start(arguments, command); - const int flags = va_arg(arguments, int); - va_end(arguments); -#endif + const int flags = static_cast(argument); std::lock_guard registry(socketsMutex); if ((flags & 1) != 0) closeOnExecDescriptors.insert(descriptor); else closeOnExecDescriptors.erase(descriptor); @@ -931,17 +1018,7 @@ int APS5_VABI fcntl_nid_postfix(int descriptor, int command, ...) { } if (command == 3) return 2 | (socket->options.nonblocking ? 4 : 0); // F_GETFL, O_RDWR if (command != 4) return Fail(22); -#ifdef _WIN32 - __builtin_sysv_va_list arguments; - __builtin_sysv_va_start(arguments, command); - const int flags = __builtin_va_arg(arguments, int); - __builtin_sysv_va_end(arguments); -#else - std::va_list arguments; - va_start(arguments, command); - const int flags = va_arg(arguments, int); - va_end(arguments); -#endif + const int flags = static_cast(argument); if ((flags & ~7) != 0) return Fail(45); #ifdef _WIN32 unsigned long enabled = (flags & 4) != 0; @@ -1065,6 +1142,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; @@ -1097,7 +1179,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); @@ -1165,7 +1249,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); @@ -1205,6 +1289,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); @@ -1321,8 +1406,80 @@ std::int64_t APS5_VABI sendmsg_nid_postfix(int descriptor, const GuestMsghdr* me } return sendto_nid_postfix(descriptor, buffer.data(), buffer.size(), flags, message->name, message->name ? message->nameLength : 0u); } +#ifdef _WIN32 +static bool GuestPagesAllow(std::uintptr_t start, std::size_t bytes, DWORD allowed) { + const auto end = start + bytes; + for (auto address = start; address < end;) { + MEMORY_BASIC_INFORMATION memory{}; + if (VirtualQuery(reinterpret_cast(address), &memory, sizeof(memory)) != sizeof(memory)) return false; + if (memory.State != MEM_COMMIT || (memory.Protect & PAGE_GUARD) != 0 || (memory.Protect & allowed) == 0) return false; + address = reinterpret_cast(memory.BaseAddress) + memory.RegionSize; + } + return true; +} +#elif defined(__linux__) +static bool Transferred(ssize_t copied, std::size_t bytes, const char* call) { + if (copied >= 0) return static_cast(copied) == bytes; + if (errno == EFAULT) return false; + throw std::runtime_error(std::string("recvmsg: ") + call + " failed with errno " + std::to_string(errno)); +} +#endif +static bool GuestRange(const void* pointer, std::size_t bytes) { + const auto start = reinterpret_cast(pointer); + return start != 0 && bytes <= UINTPTR_MAX - start; +} +static bool ReadGuest(void* destination, const void* source, std::size_t bytes) { + if (bytes == 0) return true; + if (!GuestRange(source, bytes)) return false; + GuestArena::GuestArenaPrepareHostRead_nid_no_patch(source, bytes); +#ifdef _WIN32 + if (!GuestPagesAllow(reinterpret_cast(source), bytes, + PAGE_READONLY | PAGE_READWRITE | PAGE_WRITECOPY | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY)) return false; + std::memcpy(destination, source, bytes); + return true; +#elif defined(__linux__) + iovec local{destination, bytes}; + iovec remote{const_cast(source), bytes}; + return Transferred(::process_vm_readv(::getpid(), &local, 1, &remote, 1, 0), bytes, "process_vm_readv"); +#elif defined(__APPLE__) + mach_vm_size_t copied = 0; + return ::mach_vm_read_overwrite(::mach_task_self(), reinterpret_cast(source), bytes, + reinterpret_cast(destination), &copied) == KERN_SUCCESS && copied == bytes; +#else + std::memcpy(destination, source, bytes); + return true; +#endif +} +static bool WriteGuest(void* destination, const void* source, std::size_t bytes) { + if (bytes == 0) return true; + if (!GuestRange(destination, bytes)) return false; + const GuestArena::HostWrite guard(destination, bytes); + if (!guard.Open()) return false; +#ifdef _WIN32 + if (!GuestPagesAllow(reinterpret_cast(destination), bytes, + PAGE_READWRITE | PAGE_WRITECOPY | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY)) return false; + std::memcpy(destination, source, bytes); + return true; +#elif defined(__linux__) + iovec local{const_cast(source), bytes}; + iovec remote{destination, bytes}; + return Transferred(::process_vm_writev(::getpid(), &local, 1, &remote, 1, 0), bytes, "process_vm_writev"); +#elif defined(__APPLE__) + return ::mach_vm_write(::mach_task_self(), reinterpret_cast(destination), + reinterpret_cast(source), static_cast(bytes)) == KERN_SUCCESS; +#else + std::memcpy(destination, source, bytes); + return true; +#endif +} std::int64_t APS5_VABI recvmsg_nid_postfix(int descriptor, GuestMsghdr* message, int flags) { - const auto total = MessageLength(message); + GuestMsghdr header{}; + if (!message || !ReadGuest(&header, message, sizeof(header))) return Fail(14); + if (header.iovLength < 0 || header.iovLength > 1024) return Fail(40); + std::vector vectors(static_cast(header.iovLength)); + if (!ReadGuest(vectors.data(), header.iov, vectors.size() * sizeof(GuestIovec))) return Fail(14); + header.iov = vectors.data(); + const auto total = MessageLength(&header); if (total < 0) return -1; const auto socket = Lookup(descriptor); if (!socket) return -1; @@ -1347,27 +1504,33 @@ std::int64_t APS5_VABI recvmsg_nid_postfix(int descriptor, GuestMsghdr* message, return Fail(NativeError()); #endif } - message->flags = 0; + header.flags = 0; if (received > total) { received = total; - message->flags = 0x10; + header.flags = 0x10; } std::size_t offset = 0; - for (int i = 0; i < message->iovLength && offset < static_cast(received); ++i) { - const auto count = std::min(message->iov[i].length, static_cast(received) - offset); - std::memcpy(message->iov[i].base, buffer.data() + offset, count); + for (const auto& vector : vectors) { + if (offset >= static_cast(received)) break; + const auto count = std::min(vector.length, static_cast(received) - offset); + if (!WriteGuest(vector.base, buffer.data() + offset, count)) return Fail(14); offset += count; } - if (message->name && socket->family == GuestUnix) { - if (socket->type == 1) message->nameLength = 0; else UnnamedAddress(message->name, &message->nameLength); - } else if (message->name) { - if (size > 0 && (native.ss_family == AF_INET || native.ss_family == AF_INET6)) { - GuestAddress(native, message->name, &message->nameLength); + if (header.name) { + unsigned char address[28]{}; + if (socket->family == GuestUnix) { + if (socket->type == 1) header.nameLength = 0; else UnnamedAddress(address, &header.nameLength); + } else if (size > 0 && (native.ss_family == AF_INET || native.ss_family == AF_INET6)) { + GuestAddress(native, address, &header.nameLength); } else { - message->nameLength = 0; + header.nameLength = 0; } + if (!WriteGuest(header.name, address, header.nameLength)) return Fail(14); + if (!WriteGuest(&message->nameLength, &header.nameLength, sizeof(header.nameLength))) return Fail(14); } - message->controlLength = 0; + header.controlLength = 0; + constexpr auto results = offsetof(GuestMsghdr, controlLength); + if (!WriteGuest(reinterpret_cast(message) + results, reinterpret_cast(&header) + results, sizeof(header) - results)) return Fail(14); return received; } struct GuestSendfileHeaders { diff --git a/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp b/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp index 50b6eb5d26..3955668c7f 100644 --- a/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp +++ b/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp @@ -27,6 +27,8 @@ constexpr int UMTX_OP_WAKE = 3; constexpr int UMTX_OP_WAIT_UINT = 11; constexpr int UMTX_OP_WAIT_UINT_PRIVATE = 15; constexpr int UMTX_OP_WAKE_PRIVATE = 16; +constexpr int UMTX_OP_SEM_WAIT = 19; +constexpr int UMTX_OP_SEM_WAKE = 20; constexpr int UMTX_OP_NWAKE_PRIVATE = 21; constexpr int UMTX_OP_MAX = 27; constexpr std::uint32_t UMTX_ABSTIME = 1; @@ -46,6 +48,12 @@ struct UmtxTime { std::uint32_t clockId; }; +struct UserSemaphore { + std::uint32_t hasWaiters; + std::uint32_t count; + std::uint32_t flags; +}; + struct AddressWaiter { TimedWait::Condition condition; bool woken = false; @@ -72,11 +80,10 @@ void DeferringDelivery(TFunction function) { #endif } -template -std::optional WaitOnAddressLocked(TValue* address, TValue expected, std::optional deadlineNanos, bool& woken) { - const auto key = reinterpret_cast(address); +template +std::optional WaitOnKeyLocked(std::uintptr_t key, TReady ready, std::optional deadlineNanos, bool& woken) { std::unique_lock lock(g_waitersLock); - if (std::atomic_ref(*address).load() != expected) { + if (ready()) { woken = true; return std::nullopt; } @@ -103,17 +110,22 @@ std::optional WaitOnAddressLocked(TValue* address, TVa return waited; } -template -bool WaitOnAddressUntil(TValue* address, TValue expected, std::optional deadlineNanos, const void* caller) { +template +bool WaitOnKeyUntil(std::uintptr_t key, TReady ready, std::optional deadlineNanos, const void* caller) { bool woken = false; std::optional waited; - DeferringDelivery([&] { waited = WaitOnAddressLocked(address, expected, deadlineNanos, woken); }); + DeferringDelivery([&] { waited = WaitOnKeyLocked(key, ready, deadlineNanos, woken); }); if (waited) { KernelTraceWait_nid_postfix("addr", caller, static_cast(waited->count()), !woken); } return woken; } +template +bool WaitOnAddressUntil(TValue* address, TValue expected, std::optional deadlineNanos, const void* caller) { + return WaitOnKeyUntil(reinterpret_cast(address), [&] { return std::atomic_ref(*address).load() != expected; }, deadlineNanos, caller); +} + template int WaitOnAddress(TValue* address, TValue expected, const KernelUseconds* timeout, const void* caller) { const auto deadline = timeout == nullptr ? std::nullopt : std::optional(TimedWait::DeadlineNanos(*timeout)); @@ -228,6 +240,56 @@ int UmtxNwakePrivate(void* object, std::uint64_t value) { return 0; } +void WakeSemaphoreLocked(std::uintptr_t key, UserSemaphore* semaphore) { + std::lock_guard lock(g_waitersLock); + const auto entry = g_waiters.find(key); + if (entry == g_waiters.end()) { + return; + } + auto& queue = entry->second; + if (queue.size() == 1) { + std::atomic_ref(semaphore->hasWaiters).store(0); + } + AddressWaiter* waiter = queue.front(); + queue.pop_front(); + waiter->woken = true; + waiter->condition.NotifyOne(); + if (queue.empty()) { + g_waiters.erase(entry); + } +} + +int SemWait(UserSemaphore* semaphore, const void* sizeArgument, const void* timeArgument, const void* caller) { + std::optional deadline; + if (const int error = UmtxDeadline(sizeArgument, timeArgument, deadline); error != 0) { + return error; + } + if (semaphore == nullptr) { + return GUEST_EFAULT; + } + if (!IsAlignedAddress(reinterpret_cast(semaphore))) { + APS5_INVALID_ARG_EX; + } + const bool woken = WaitOnKeyUntil(reinterpret_cast(semaphore), [&] { + std::uint32_t idle = 0; + std::atomic_ref(semaphore->hasWaiters).compare_exchange_strong(idle, 1); + return std::atomic_ref(semaphore->count).load() != 0; + }, deadline, caller); + return woken ? 0 : GUEST_ETIMEDOUT; +} + +int SemWake(UserSemaphore* semaphore) { + if (semaphore == nullptr) { + return GUEST_EFAULT; + } + if (!IsAlignedAddress(reinterpret_cast(semaphore))) { + APS5_INVALID_ARG_EX; + } + const auto key = reinterpret_cast(semaphore); + DeferringDelivery([&] { WakeSemaphoreLocked(key, semaphore); }); + return 0; +} + } // namespace extern "C" { @@ -300,12 +362,16 @@ int APS5_VABI _umtx_op_nid_postfix(void* object, int operation, std::uint64_t va case UMTX_OP_NWAKE_PRIVATE: error = UmtxNwakePrivate(object, value); break; + case UMTX_OP_SEM_WAIT: + error = SemWait(static_cast(object), sizeArgument, timeArgument, caller); + break; + case UMTX_OP_SEM_WAKE: + error = SemWake(static_cast(object)); + break; default: if (operation < 0 || operation >= UMTX_OP_MAX) { error = GUEST_EINVAL; - } else if (operation == UMTX_OP_RESERVED1) { - NotImplemented_nid_no_patch("_umtx_op operation 1"); - } else if (operation < UMTX_OP_RESERVED1) { + } else if (operation <= UMTX_OP_RESERVED1) { error = GUEST_EOPNOTSUPP; } else { NotImplemented_nid_no_patch(("_umtx_op operation " + std::to_string(operation)).c_str()); diff --git a/core/libs/prx/libkernel/System/src/Exception.cpp b/core/libs/prx/libkernel/System/src/Exception.cpp index 6dbef36c89..b7eb4f65f7 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); @@ -135,6 +192,7 @@ void CallHandler(GuestExceptionHandler handler, int signum, GuestUcontext& ucont constexpr std::size_t RedZone = 128; constexpr std::size_t HomeArea = 32; constexpr auto RetryLimit = std::chrono::seconds(1); +constexpr auto HostCodeLimit = std::chrono::milliseconds(50); constexpr std::size_t VectorBytes = 16 * 32; struct Delivery { @@ -249,6 +307,94 @@ 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; }); +} + +DWORD64 NtdllBase() { + static const auto ntdll = reinterpret_cast(GetModuleHandleW(L"ntdll.dll")); + return ntdll; +} + +bool InSystemCall(DWORD64 rip, DWORD64 base) { + if (base == 0 || base != NtdllBase() || rip < base + 2) return false; + const auto* code = reinterpret_cast(rip - 2); + return code[0] == 0x0f && code[1] == 0x05; +} + +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{}; @@ -259,7 +405,15 @@ bool RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) { const auto native = static_cast(thread->nativeHandle); 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; + const auto start = std::chrono::steady_clock::now(); + const auto retryDeadline = start + RetryLimit; + auto hostDeadline = start + HostCodeLimit; + static_cast(RestoringContext(0)); + static_cast(InWinpthread(0)); + static_cast(BlockedInWait(0)); + static_cast(ExecutableBase()); + static_cast(Images()); + static_cast(NtdllBase()); for (;;) { if (SuspendThread(native) == static_cast(-1)) { if (Exited(native)) return false; @@ -281,9 +435,18 @@ 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); + const auto now = std::chrono::steady_clock::now(); + if (running == CodeKind::Host && InSystemCall(delivery.context.Rip, image) && now < retryDeadline) hostDeadline = std::max(hostDeadline, std::min(retryDeadline, now + HostCodeLimit)); + if (!restoring && (running == CodeKind::Guest || (running == CodeKind::Host && now >= hostDeadline))) 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 +678,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 37239729ee..348464a177 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -142,7 +142,11 @@ std::atomic guestNice{0}; constexpr int guestNiceMin = -20; constexpr int guestNiceMax = 20; constexpr int guestRlimitData = 2; +constexpr int guestRlimitStack = 3; constexpr int guestRlimitCore = 4; +constexpr int guestRlimitRss = 5; +constexpr int guestRlimitMemlock = 6; +constexpr int guestRlimitNproc = 7; constexpr int guestRlimitNofile = 8; constexpr int guestRlimitSbsize = 9; constexpr int guestRlimitVmem = 10; @@ -152,6 +156,8 @@ constexpr int guestRlimitKqueues = 13; constexpr int guestRlimitUmtxp = 14; constexpr int guestRlimitCount = 15; constexpr std::int64_t guestRlimitInfinity = std::numeric_limits::max(); +constexpr std::int64_t guestStackLimit = 0x200000; +constexpr std::int64_t guestProcessLimit = 1604; #ifdef _WIN32 std::int64_t hostMemoryLimit() { @@ -357,6 +363,8 @@ int APS5_VABI setrlimit_nid_postfix(int resource, const GuestResourceLimit* limi return 0; } } + if (resource == guestRlimitNproc) + throw std::runtime_error("setrlimit: RLIMIT_NPROC is not stored"); if (requested.rlim_cur != current.rlim_cur || requested.rlim_max != current.rlim_max) throw std::runtime_error("setrlimit: changing the limit of resource " + std::to_string(resource) + " is not supported"); return 0; @@ -445,6 +453,16 @@ int APS5_VABI sceKernelUuidCreate(std::uint32_t* uuid) { return 0; } +int APS5_VABI sceKernelCreateUuidV4(std::uint8_t* uuid) { + if (!uuid) return sceInvalidArgument; + std::array value; + HostEntropy_nid_no_patch(value.data(), value.size()); + value[6] = static_cast((value[6] & 0x0fu) | 0x40u); + value[8] = static_cast((value[8] & 0x3fu) | 0x80u); + std::memcpy(uuid, value.data(), value.size()); + return 0; +} + void APS5_VABI sceKernelSync(void) { SyncWrittenPaths_nid_no_patch(); } @@ -630,9 +648,19 @@ int APS5_VABI getrlimit_nid_postfix(int resource, GuestResourceLimit* limit) { *limit = *guestOpenFileLimit; return 0; } + case guestRlimitStack: + limit->rlim_cur = guestStackLimit; + limit->rlim_max = guestStackLimit; + return 0; + case guestRlimitNproc: + limit->rlim_cur = guestProcessLimit; + limit->rlim_max = guestProcessLimit; + return 0; case guestRlimitCpu: case guestRlimitFsize: case guestRlimitCore: + case guestRlimitRss: + case guestRlimitMemlock: case guestRlimitSbsize: case guestRlimitVmem: case guestRlimitNpts: @@ -718,6 +746,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/Signals.cpp b/core/libs/prx/libkernel/System/src/Signals.cpp index 36678ba196..231d3d2643 100644 --- a/core/libs/prx/libkernel/System/src/Signals.cpp +++ b/core/libs/prx/libkernel/System/src/Signals.cpp @@ -38,6 +38,7 @@ constexpr int MaxSignal = 128; constexpr int GuestSigkill = 9; constexpr int GuestSigstop = 17; constexpr int GuestSigalrm = 14; +constexpr int GuestSigcont = 19; constexpr int SaRestart = 0x2; constexpr int SaSiginfo = 0x40; std::atomic handlers[MaxSignal + 1]{}; @@ -75,6 +76,9 @@ struct GuestTimespec { std::int64_t nanoseconds; }; static_assert(sizeof(GuestTimespec) == 16 && offsetof(GuestTimespec, nanoseconds) == 8); +bool IgnoredByDefault(int guest) { + return guest == 16 || guest == GuestSigcont || guest == 20 || guest == 23 || guest == 28 || guest == 29; +} std::uint32_t SignalBit(int guest) { return 1u << ((guest - 1) % 32); } std::uint32_t SignalWord(int guest) { return static_cast((guest - 1) / 32); } bool SignalInSet(const GuestSignalSet& set, int guest) { return (set.bits[SignalWord(guest)] & SignalBit(guest)) != 0; } @@ -231,15 +235,16 @@ bool DeliverPendingSignal(int guest) { int WaitForPendingInSet(const GuestSignalSet& set, bool waitForever, const GuestTimespec* timeout, int* guest) { auto& pending = Pending(); std::unique_lock lock(pending.lock); + std::optional deadline; for (;;) { if (PendingInSet(set, guest)) return 0; if (waitForever) { pending.changed.wait(lock); continue; } - const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(timeout->seconds) + + if (!deadline) deadline = std::chrono::steady_clock::now() + std::chrono::seconds(timeout->seconds) + std::chrono::nanoseconds(timeout->nanoseconds); - if (pending.changed.wait_until(lock, deadline) == std::cv_status::no_timeout) continue; + if (pending.changed.wait_until(lock, *deadline) == std::cv_status::no_timeout) continue; return PendingInSet(set, guest) ? 0 : GuestEagain; } } @@ -271,6 +276,7 @@ extern "C" int APS5_VABI _sigprocmask_nid_postfix(int how, const GuestSignalSet* } namespace { +constexpr std::int64_t MaxItimerSeconds = 0x7fffffff / 2; struct AlarmState { std::mutex lock; std::condition_variable changed; @@ -347,6 +353,10 @@ void NotifyAlarms(AlarmState& alarms) { } extern "C" unsigned int APS5_VABI GuestAlarm_nid_no_patch(unsigned int seconds) { + if (seconds > MaxItimerSeconds) { + *__error_nid_postfix() = 22; + return static_cast(-1); + } auto& alarms = Alarms(); std::lock_guard lock(alarms.lock); const auto now = std::chrono::steady_clock::now(); @@ -399,7 +409,6 @@ extern "C" int APS5_VABI getitimer_nid_postfix(int which, GuestItimerval* output namespace { constexpr int ItimerReal = 0; constexpr int ItimerProf = 2; -constexpr std::int64_t MaxItimerSeconds = 0x7fffffff / 2; constexpr std::int64_t MinExactIntervalMicros = 1000; bool ValidItimer(const KernelTimeval& time) { @@ -491,13 +500,16 @@ int APS5_VABI sigaction_nid_postfix(int guest, const GuestSigaction* action, Gue return 0; } int APS5_VABI raise_nid_postfix(int guest) { + if (guest == 0) return 0; if (guest < 1 || guest > MaxSignal) { *__error_nid_postfix() = 22; return -1; } const GuestSigaction action = Disposition(guest); if (action.handler > 1 && !Delivered(action)) throw std::runtime_error("raise: SA_SIGINFO handlers are not delivered"); + if (action.handler == 1 || (action.handler == 0 && guest != GuestSigcont && IgnoredByDefault(guest))) return 0; if (SignalBlocked(guest)) { - if (action.handler != 1) MarkPending(guest); + MarkPending(guest); return 0; } + if (action.handler == 0 && IgnoredByDefault(guest)) return 0; const int native = NativeSignal(guest); if (native != 0) { const int result = std::raise(native); @@ -521,9 +533,9 @@ int APS5_VABI killpg_nid_postfix(int pgrp, int guest) { int APS5_VABI pthread_kill_nid_postfix(Pthread thread, int guest) { if (guest < 0 || guest > MaxSignal) return PosixThread::GUEST_EINVAL; if (thread == nullptr) return PosixThread::GUEST_ESRCH; - if (guest == 0) return 0; - if (thread == scePthreadSelf()) return raise_nid_postfix(guest) == 0 ? 0 : *__error_nid_postfix(); + if (thread == scePthreadSelf()) return guest == 0 || raise_nid_postfix(guest) == 0 ? 0 : *__error_nid_postfix(); if (!GuestThreadIsLive(thread)) return PosixThread::GUEST_ESRCH; + if (guest == 0) return 0; if (Disposition(guest).handler != 1) MarkPending(guest); return 0; } diff --git a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp index a9af1155db..da87fe7bb2 100644 --- a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp +++ b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp @@ -1,14 +1,18 @@ +#include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libkernel/DirectMemory/DirectMemory.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/System/include/HostCpu.hpp" +#include "SceTypes.hpp" #include +#include #include #include #include #include #include #include +#include #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN #include @@ -30,6 +34,7 @@ struct SceKernelSystemSwVersion { char versionString[0x1C]; std::uint32_t version; }; +extern "C" int APS5_VABI sceKernelVirtualQuery(const void* addr, int flags, VirtualQueryInfo* info, uint64_t info_size); namespace { @@ -62,6 +67,16 @@ constexpr SystemString SystemStrings[] = { {{6, 1}, "hw.machine", "amd64"}, }; +struct SystemInteger { + int mib[2]; + const char* name; + std::int32_t value; +}; + +constexpr SystemInteger SystemIntegers[] = { + {{1, 24}, "kern.osreldate", 1104001}, +}; + const SystemString* FindSystemString(const int* name) { for (const auto& entry : SystemStrings) { if (name[0] == entry.mib[0] && name[1] == entry.mib[1]) return &entry; @@ -69,8 +84,97 @@ const SystemString* FindSystemString(const int* name) { return nullptr; } +const SystemInteger* FindSystemInteger(const int* name) { + for (const auto& entry : SystemIntegers) { + if (name[0] == entry.mib[0] && name[1] == entry.mib[1]) return &entry; + } + return nullptr; +} + 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 FindSystemInteger(name) != nullptr || (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 { + std::int32_t structSize; + std::int32_t type; + std::uint64_t start; + std::uint64_t end; + std::uint64_t offset; + std::uint64_t vnodeFileId; + std::uint32_t vnodeFsid; + std::int32_t flags; + std::int32_t resident; + std::int32_t privateResident; + std::int32_t protection; + std::int32_t referenceCount; + std::int32_t shadowCount; + std::int32_t vnodeType; + std::uint64_t vnodeSize; + std::uint32_t vnodeDevice; + std::uint16_t vnodeMode; + std::uint16_t status; + std::int32_t spare[12]; + char path[8]; +}; +static_assert(offsetof(GuestVmEntry, start) == 8 && offsetof(GuestVmEntry, protection) == 56 && offsetof(GuestVmEntry, path) == 136 && sizeof(GuestVmEntry) == 144); + +std::vector GuestVmEntries() { + std::vector entries; + std::uintptr_t start = 0; + std::uintptr_t end = 0; + for (std::uintptr_t cursor = 0; GuestArena::GuestArenaNextUsed_nid_no_patch(cursor, &start, &end); cursor = end) { + GuestVmEntry entry{}; + entry.structSize = sizeof(entry); + entry.type = VmEntryDefaultType; + entry.start = start; + entry.end = end; + VirtualQueryInfo info{}; + if (sceKernelVirtualQuery(reinterpret_cast(start), 0, &info, sizeof(info)) == 0) entry.protection = info.protection & 7; + entries.push_back(entry); + } + return entries; +} + +bool IsUnansweredMib(const int* name, std::uint32_t nameLength) { + if (nameLength != 2) return false; + return (name[0] == 1 && (name[1] == 6 || name[1] == 7 || name[1] == 12 || name[1] == 27 || name[1] == 32)) || (name[0] == 6 && (name[1] == 4 || name[1] == 11)); +} + +bool IsUnansweredName(const char* name) { + static constexpr const char* unanswered[] = { + "hw.byteorder", + "hw.cacheline_size", + "hw.machine_arch", + "hw.usermem", + "kern.clockrate", + "kern.maxfiles", + "kern.maxfilesperproc", + "kern.maxproc", + "kern.ps_strings", + "kern.sched.name", + "kern.smp.maxcpus", + "kern.stackprot", + "kern.timecounter.hardware", + }; + return std::any_of(std::begin(unanswered), std::end(unanswered), [name](const char* entry) { return std::strcmp(name, entry) == 0; }); } } @@ -139,6 +243,16 @@ 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 (IsUnansweredMib(name, nameLength)) { + if (newValue != nullptr) throw std::runtime_error(std::string(__func__) + ": writing MIB " + std::to_string(name[0]) + "." + std::to_string(name[1])); + *__error_nid_postfix() = 2; + return -1; + } if (nameLength == 2 && name[0] == 6 && name[1] == 7) { if (newValue != nullptr) { *__error_nid_postfix() = 1; @@ -184,6 +298,13 @@ int APS5_VABI sysctl_nid_postfix(const int* name, std::uint32_t nameLength, void } return CopyOut(entry->value, std::strlen(entry->value) + 1, oldValue, oldLength); } + if (const auto* entry = FindSystemInteger(name)) { + if (newValue != nullptr) { + *__error_nid_postfix() = 1; + return -1; + } + return CopyOut(&entry->value, sizeof(entry->value), oldValue, oldLength); + } } if (nameLength >= 3 && name[0] == 1 && name[1] == 14 && name[2] == 12) { if (newValue != nullptr) { @@ -202,6 +323,22 @@ int APS5_VABI sysctl_nid_postfix(const int* name, std::uint32_t nameLength, void static constexpr char path[] = "/app0/eboot.bin"; return CopyOut(path, sizeof(path), oldValue, oldLength); } + if (nameLength >= 3 && name[0] == 1 && name[1] == 14 && name[2] == 32) { + if (newValue != nullptr) { + *__error_nid_postfix() = 1; + return -1; + } + if (nameLength != 4) { + *__error_nid_postfix() = 22; + return -1; + } + if (name[3] != getpid_nid_postfix()) { + *__error_nid_postfix() = 3; + return -1; + } + const auto entries = GuestVmEntries(); + return CopyOut(entries.data(), entries.size() * sizeof(GuestVmEntry), oldValue, oldLength); + } if (nameLength == 2 && name[0] == 1 && name[1] == 37) { if (newValue != nullptr) { *__error_nid_postfix() = 1; @@ -217,6 +354,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; @@ -238,6 +384,14 @@ int APS5_VABI sysctlbyname_nid_postfix(const char* name, void* oldValue, std::si for (const auto& entry : SystemStrings) { if (std::strcmp(name, entry.name) == 0) return sysctl_nid_postfix(entry.mib, 2, oldValue, oldLength, newValue, newLength); } + for (const auto& entry : SystemIntegers) { + if (std::strcmp(name, entry.name) == 0) return sysctl_nid_postfix(entry.mib, 2, oldValue, oldLength, newValue, newLength); + } + if (IsUnansweredName(name)) { + if (newValue != nullptr) throw std::runtime_error(std::string(__func__) + ": writing " + name); + *__error_nid_postfix() = 2; + return -1; + } if (std::strcmp(name, "kern.proc.pathname") == 0) { static constexpr int mib[] = {1, 14, 12}; return sysctl_nid_postfix(mib, 3, oldValue, oldLength, newValue, newLength); @@ -251,11 +405,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"); @@ -318,13 +472,21 @@ int APS5_VABI sysctlnametomib_nid_postfix(const char* name, int* mib, std::size_ std::size_t length; }; static constexpr KnownName known[] = { + {"hw.byteorder", {6, 4}, 2}, + {"hw.machine_arch", {6, 11}, 2}, {"hw.model", {6, 2}, 2}, {"hw.ncpu", {6, 3}, 2}, {"hw.pagesize", {6, 7}, 2}, {"hw.physmem", {6, 5}, 2}, {"hw.realmem", {6, 12}, 2}, {"kern.arandom", {1, 37}, 2}, + {"kern.clockrate", {1, 12}, 2}, + {"kern.maxfiles", {1, 7}, 2}, + {"kern.maxfilesperproc", {1, 27}, 2}, + {"kern.maxproc", {1, 6}, 2}, {"kern.proc.pathname", {1, 14, 12}, 3}, + {"kern.ps_strings", {1, 32}, 2}, + {"kern.rng_pseudo", {1, 50}, 2}, }; constexpr std::size_t MaximumPath = 1024; if (name == nullptr || size == nullptr) throw std::invalid_argument(std::string(__func__) + ": null name or size"); @@ -333,7 +495,7 @@ int APS5_VABI sysctlnametomib_nid_postfix(const char* name, int* mib, std::size_ *__error_nid_postfix() = 63; return -1; } - if (nameLength == 0 || std::strcmp(name, "machdep.cacheline_size") == 0) { + if (nameLength == 0 || std::strcmp(name, "hw.cacheline_size") == 0 || std::strcmp(name, "hw.usermem") == 0 || std::strcmp(name, "machdep.cacheline_size") == 0) { *__error_nid_postfix() = 2; return -1; } @@ -349,6 +511,9 @@ int APS5_VABI sysctlnametomib_nid_postfix(const char* name, int* mib, std::size_ for (const auto& entry : SystemStrings) { if (std::strcmp(name, entry.name) == 0) return copyMib(entry.mib, 2); } + for (const auto& entry : SystemIntegers) { + if (std::strcmp(name, entry.name) == 0) return copyMib(entry.mib, 2); + } throw std::runtime_error(std::string(__func__) + ": unsupported name " + name); } } diff --git a/core/libs/prx/libkernel/Time/SleepWake.cpp b/core/libs/prx/libkernel/Time/SleepWake.cpp new file mode 100644 index 0000000000..a6a774e8cb --- /dev/null +++ b/core/libs/prx/libkernel/Time/SleepWake.cpp @@ -0,0 +1,57 @@ +#include "prx/libkernel/Time/include/SleepWake.hpp" +#include "prx/libkernel/Time/include/TimedWait.hpp" + +#include +#include + +namespace SleepWake { + +namespace { + +struct ThreadState { + unsigned earlyWakes = 0; + std::uint64_t lastSleepEndNanos = 0; + bool sleeping = false; +}; + +thread_local ThreadState state; + +std::atomic completions{0}; + +bool Enabled() { + static const bool enabled = std::getenv("APS5_NO_SLEEP_WAKE") == nullptr; + return enabled; +} + +} + +void Sleep(std::uint64_t nanos) { + bool woken = false; + if (nanos != 0 && nanos <= MAX_WAKEABLE_NANOS && !state.sleeping && Enabled()) { + if (TimedWait::NowNanos() - state.lastSleepEndNanos >= nanos) state.earlyWakes = 0; + if (state.earlyWakes < EARLY_WAKES_PER_RUN) { + state.sleeping = true; + const bool early = TimedWait::SleepNanosUnlessChanged(nanos, completions, completions.load(std::memory_order_seq_cst)); + state.sleeping = false; + if (early) ++state.earlyWakes; + woken = true; + } + } + if (!woken) TimedWait::SleepNanos(nanos); + state.lastSleepEndNanos = TimedWait::NowNanos(); +} + +} + +extern "C" { + +void KernelWakeSleepers_nid_no_patch() { + SleepWake::completions.fetch_add(1, std::memory_order_seq_cst); + TimedWait::WakeChangeSleepers(); +} + +void KernelNoteGpuSubmit_nid_no_patch() { + SleepWake::state.earlyWakes = 0; +} + +} diff --git a/core/libs/prx/libkernel/Time/Time.cpp b/core/libs/prx/libkernel/Time/Time.cpp index 4c447a1a45..67ed023339 100644 --- a/core/libs/prx/libkernel/Time/Time.cpp +++ b/core/libs/prx/libkernel/Time/Time.cpp @@ -1,5 +1,6 @@ #include "prx/libkernel/Time/include/Time.hpp" #include "prx/libkernel/Time/include/TimedWait.hpp" +#include "prx/libkernel/Time/include/SleepWake.hpp" #include "prx/libkernel/Pthread/include/Cancel.hpp" #include "prx/libc/include/General.hpp" @@ -176,12 +177,13 @@ std::uint64_t APS5_VABI sceKernelGetProcessTime() { } std::uint64_t APS5_VABI sceKernelGetProcessTimeCounter() { - const std::uint64_t start = GetStartNanos(); - return GetMonotonicNanos() - start; + const std::uint64_t nanos = GetMonotonicNanos() - GetStartNanos(); + const std::uint64_t frequency = sceKernelGetTscFrequency(); + return nanos / 1000000000ULL * frequency + nanos % 1000000000ULL * frequency / 1000000000ULL; } std::uint64_t APS5_VABI sceKernelGetProcessTimeCounterFrequency() { - return 1000000000ULL; + return sceKernelGetTscFrequency(); } static std::string DescribeCallSite(const void* caller) { @@ -274,7 +276,7 @@ void KernelTraceWait_nid_postfix(const char* kind, const void* caller, std::uint static void CancellableSleep(std::uint64_t nanos) { ThreadCancel::Check(); - TimedWait::SleepNanos(nanos); + SleepWake::Sleep(nanos); ThreadCancel::Check(); } diff --git a/core/libs/prx/libkernel/Time/TimedWait.cpp b/core/libs/prx/libkernel/Time/TimedWait.cpp index c4ae764f64..7c171db399 100644 --- a/core/libs/prx/libkernel/Time/TimedWait.cpp +++ b/core/libs/prx/libkernel/Time/TimedWait.cpp @@ -1,9 +1,11 @@ #include "prx/libkernel/Time/include/TimedWait.hpp" #include "prx/libkernel/Pthread/include/WindowsThreadLocal.hpp" +#include #include #include #include +#include #ifdef _WIN32 #include @@ -19,6 +21,28 @@ namespace { constexpr std::uint64_t YIELD_BELOW_NANOS = 50000ULL; constexpr std::uint64_t LEAD_NANOS = 500000ULL; +std::mutex& ChangeSleepersMutex() { + static std::mutex mutex; + return mutex; +} + +std::atomic& ChangeSleeperCount() { + static std::atomic count{0}; + return count; +} + +#ifdef _WIN32 +std::vector& ChangeSleepers() { + static std::vector sleepers; + return sleepers; +} +#else +std::condition_variable& ChangeCondition() { + static std::condition_variable condition; + return condition; +} +#endif + struct ThreadWaitState { std::atomic own{0}; std::atomic* bound = &own; @@ -114,12 +138,14 @@ struct Waiter { Waiter* next = nullptr; Waiter* nested = nullptr; const void* key = nullptr; + HANDLE wake = nullptr; bool queued = false; bool busy = false; ~Waiter() { delete nested; if (event) CloseHandle(event); + if (wake) CloseHandle(wake); if (timer) CloseHandle(timer); for (const auto sleepTimer : sleepTimers) if (sleepTimer) CloseHandle(sleepTimer); } @@ -426,6 +452,72 @@ void SleepUntil(std::uint64_t deadlineNanos) { #endif } +bool SleepNanosUnlessChanged(std::uint64_t nanos, const std::atomic& counter, std::uint64_t observed) { + const std::uint64_t deadlineNanos = NowNanos() + nanos; +#ifdef _WIN32 + if (Coarse() || nanos < YIELD_BELOW_NANOS) { + SleepNanos(nanos); + return false; + } + Waiter* waiter = ThisThreadWaiter(); + if (!waiter->wake) waiter->wake = CreateEventW(nullptr, FALSE, FALSE, nullptr); + if (!waiter->wake) throw std::runtime_error("TimedWait: CreateEventW failed for a wakeable sleep"); + { + std::lock_guard lock(ChangeSleepersMutex()); + ChangeSleepers().push_back(waiter->wake); + } + ChangeSleeperCount().fetch_add(1, std::memory_order_seq_cst); + struct Unregister { + HANDLE wake; + ~Unregister() { + ChangeSleeperCount().fetch_sub(1, std::memory_order_seq_cst); + std::lock_guard lock(ChangeSleepersMutex()); + auto& sleepers = ChangeSleepers(); + sleepers.erase(std::find(sleepers.begin(), sleepers.end(), wake)); + } + } unregister{waiter->wake}; + ResetEvent(waiter->wake); + std::uint64_t now = NowNanos(); + if (counter.load(std::memory_order_seq_cst) != observed) return true; + if (deadlineNanos > now + LEAD_NANOS) { + const HANDLE handles[2] = {ThreadTimer(1), waiter->wake}; + LARGE_INTEGER due{}; + due.QuadPart = -static_cast((deadlineNanos - now - LEAD_NANOS) / 100ULL); + if (due.QuadPart == 0) due.QuadPart = -1; + if (handles[0] && SetWaitableTimer(handles[0], &due, 0, nullptr, nullptr, FALSE)) { + if (AlertableWait(2, handles, INFINITE) == WAIT_OBJECT_0 + 1) CancelWaitableTimer(handles[0]); + } else { + AlertableWait(1, &handles[1], WholeMilliseconds(deadlineNanos - now - LEAD_NANOS)); + } + if (counter.load(std::memory_order_seq_cst) != observed) return true; + now = NowNanos(); + } + while (now < deadlineNanos) { + if (counter.load(std::memory_order_seq_cst) != observed) return true; + YieldProcessor(); + now = NowNanos(); + } + return false; +#else + std::unique_lock lock(ChangeSleepersMutex()); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::nanoseconds(nanos); + ChangeSleeperCount().fetch_add(1, std::memory_order_seq_cst); + const bool changed = ChangeCondition().wait_until(lock, deadline, [&] { return counter.load(std::memory_order_seq_cst) != observed; }); + ChangeSleeperCount().fetch_sub(1, std::memory_order_seq_cst); + return changed; +#endif +} + +void WakeChangeSleepers() { + if (ChangeSleeperCount().load(std::memory_order_seq_cst) == 0) return; + std::lock_guard lock(ChangeSleepersMutex()); +#ifdef _WIN32 + for (const HANDLE sleeper : ChangeSleepers()) SetEvent(sleeper); +#else + ChangeCondition().notify_all(); +#endif +} + void SleepNanos(std::uint64_t nanos) { if (nanos == 0) return; #ifdef _WIN32 diff --git a/core/libs/prx/libkernel/Time/include/SleepWake.hpp b/core/libs/prx/libkernel/Time/include/SleepWake.hpp new file mode 100644 index 0000000000..47185c0cb1 --- /dev/null +++ b/core/libs/prx/libkernel/Time/include/SleepWake.hpp @@ -0,0 +1,22 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_TIME_INCLUDE_SLEEPWAKE_HPP +#define CORE_LIBS_PRX_LIBKERNEL_TIME_INCLUDE_SLEEPWAKE_HPP + +#include + +namespace SleepWake { + +constexpr std::uint64_t MAX_WAKEABLE_NANOS = 1000000ULL; +constexpr unsigned EARLY_WAKES_PER_RUN = 8; + +void Sleep(std::uint64_t nanos); + +} + +extern "C" { + +void KernelWakeSleepers_nid_no_patch(); +void KernelNoteGpuSubmit_nid_no_patch(); + +} + +#endif diff --git a/core/libs/prx/libkernel/Time/include/Time.hpp b/core/libs/prx/libkernel/Time/include/Time.hpp index 925140891d..3d292a09a6 100644 --- a/core/libs/prx/libkernel/Time/include/Time.hpp +++ b/core/libs/prx/libkernel/Time/include/Time.hpp @@ -10,6 +10,7 @@ extern "C" { std::uint64_t APS5_VABI sceKernelGetProcessTime(); std::uint64_t APS5_VABI sceKernelGetProcessTimeCounter(); std::uint64_t APS5_VABI sceKernelGetProcessTimeCounterFrequency(); + std::uint64_t APS5_VABI sceKernelGetTscFrequency(); int APS5_VABI sceKernelUsleep_nid_postfix(KernelUseconds microseconds); int APS5_VABI sceKernelNanosleep(const KernelTimespec* rqtp, KernelTimespec* rmtp); int APS5_VABI nanosleep_nid_postfix(const KernelTimespec* rqtp, KernelTimespec* rmtp); diff --git a/core/libs/prx/libkernel/Time/include/TimedWait.hpp b/core/libs/prx/libkernel/Time/include/TimedWait.hpp index 446b0c73f3..2f1725819d 100644 --- a/core/libs/prx/libkernel/Time/include/TimedWait.hpp +++ b/core/libs/prx/libkernel/Time/include/TimedWait.hpp @@ -16,6 +16,8 @@ std::uint64_t DeadlineNanos(std::uint64_t microseconds); std::uint64_t RemainingMicros(std::uint64_t deadlineNanos); void SleepNanos(std::uint64_t nanos); void SleepUntil(std::uint64_t deadlineNanos); +bool SleepNanosUnlessChanged(std::uint64_t nanos, const std::atomic& counter, std::uint64_t observed); +void WakeChangeSleepers(); struct Waiter; diff --git a/core/libs/prx/libkernel/Unimplemented.cpp b/core/libs/prx/libkernel/Unimplemented.cpp index fbb3d6935a..3dd75f72d8 100644 --- a/core/libs/prx/libkernel/Unimplemented.cpp +++ b/core/libs/prx/libkernel/Unimplemented.cpp @@ -10,6 +10,16 @@ int APS5_VABI __tls_get_addr_nid_postfix(void) { return 0; } +int APS5_VABI physhm_open_nid_postfix(void) { + NotImplemented_nid_no_patch("physhm_open"); + return 0; +} + +int APS5_VABI physhm_unlink_nid_postfix(void) { + NotImplemented_nid_no_patch("physhm_unlink"); + return 0; +} + APS5_EXPORT("AC1FtjqMCL0", sceKernelUnknown03); int APS5_VABI sceKernelUnknown03(void) { NotImplemented_nid_no_patch("AC1FtjqMCL0"); @@ -111,4 +121,15 @@ int APS5_VABI sceKernelUnknown19(void) { NotImplemented_nid_no_patch("uDGXwUKqi8s"); return 0; } + +int APS5_VABI sceKernelPrintBacktraceWithModuleInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetProcessAbiType() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libkernel_sys/CMakeLists.txt b/core/libs/prx/libkernel_sys/CMakeLists.txt new file mode 100644 index 0000000000..6938d0c456 --- /dev/null +++ b/core/libs/prx/libkernel_sys/CMakeLists.txt @@ -0,0 +1,3 @@ +add_library(${LIB_NAME} SHARED EXCLUDE_FROM_ALL Export.cpp Export.Discovery.cpp) +target_include_directories(${LIB_NAME} PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(${LIB_NAME}) diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp new file mode 100644 index 0000000000..8b3da583e4 --- /dev/null +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -0,0 +1,2641 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI aio_cancel_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI aio_error_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI aio_fsync_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI aio_read_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI aio_return_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI aio_suspend_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI aio_waitcomplete_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI aio_write_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI amd64_set_fsbase_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI blockpool_batch_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI blockpool_map_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI blockpool_open_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI blockpool_unmap_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI chown_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI cpuset_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI cpuset_getid_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI cpuset_setid_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI dynlib_get_obj_member_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI fchownat_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI fstat_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI futimesat_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI get_authinfo_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI get_module_info_list_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI get_page_table_stats_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI get_sdk_compiled_version_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI get_self_auth_info_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI get_vm_map_timestamp_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI getfsstat_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI getlogin_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ipmimgr_call_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI is_in_sandbox_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI kenv_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI kldfind_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI kldfirstmod_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI kldload_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI kldnext_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI kldstat_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI kldsym_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI kldunload_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI kmq_notify_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI kmq_open_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI kmq_setattr_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI kmq_timedreceive_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI kmq_timedsend_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI kmq_unlink_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ksem_close_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ksem_destroy_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ksem_getvalue_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ksem_init_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ksem_open_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ksem_post_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ksem_timedwait_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ksem_trywait_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ksem_unlink_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ksem_wait_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ktimer_create_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ktimer_delete_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ktimer_getoverrun_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ktimer_gettime_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ktimer_settime_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ktrace_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI lchflags_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI lchmod_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI lchown_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI linkat_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI mdbg_call_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI mdbg_service_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI mount_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI openintr_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI profil_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pselect_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_attr_getaffinity_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_attr_getsolosched_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_attr_setaffinity_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_attr_setcreatesuspend_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_attr_setstackaddr_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_barrier_setname_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_cond_setname_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_get_specificarray_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_get_user_context_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_getstack_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_main_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_multi_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_mutex_getprioceiling_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_mutex_init_for_mono_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_mutex_setname_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_mutex_setprioceiling_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_mutexattr_getgen_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_mutexattr_setgen_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_resume_all_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_resume_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_resume_user_context_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_rwlock_reltimedrdlock_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_rwlock_reltimedwrlock_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_rwlock_setname_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_rwlockattr_settype_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_set_defaultstacksize_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_set_user_context_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_single_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_suspend_all_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_suspend_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_suspend_user_context_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_switch_add_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_switch_delete_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pthread_timedjoin_np_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ptrace_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI pwrite_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI reboot_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI revoke_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI rfork_thread_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI rtld_printf_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCoredumpDebugForceCoredumpOnAppClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCoredumpDebugTriggerCoredump(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCoredumpGetStopInfoGpu(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCoredumpInternalGetThreadContextInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelAddACInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelAddCpumodeEvent(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelAddGpuExceptionEvent(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelAddProcessToCanvasMap(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelAddResource(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelAllocateDirectMemory2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelAllocateDirectMemoryForApp(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelAllocateDirectMemoryForMiniApp(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelAllocateToolMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelAllocateTraceDirectMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelAprCtrl(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelAprSubmitCommandBufferAndGetResult_TEST(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelAprSubmitCommandBuffer_TEST(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelAvailableToolMemorySize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelBacktraceSelf(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelBeginAppMount(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelBeginAppMount2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelCallIndirectBuffer(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelCallIndirectBuffer2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelClearBootReqNotifyCount(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelClearGameDirectMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelCloseEport(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelCloseEventFlag(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelCloseSema(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelCreateBudget(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelCreateEport(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelDebugAcquireAndUpdateDebugRegister(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelDebugGetAppStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelDebugGetPauseCount(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelDebugGetPrivateLogText(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelDebugGetSchedLockMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelDebugGetSdkLogText(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelDebugGpuPaDebugIsInProgress(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelDebugInjectProcessEvent(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelDebugPackageCorrupted(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelDebugRaiseExceptionWithContext(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelDebugRaiseExceptionWithInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelDebugReleaseDebugContext(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelDeleteACInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelDeleteBudget(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelDeleteCpumodeEvent(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelDeleteEport(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelDeleteGpuExceptionEvent(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelDeleteProcessFromCanvasMap(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelDeleteSavedApp(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelDirectMemoryQueryForId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelEnableDmemAliasing2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelEnablePthreadObjectCheck(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelEndAppMount(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelEndAppMount2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelEventLogInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelEventLogPread(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelEventLogRead(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelEventLogWrite(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelExpectedHddFormat(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelFlock(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetACInfoAllForCoredump(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetAppCategoryType(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetAppInfoByAppId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetAppState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetAslrStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetAutoUpdateVersion(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetBackupRestoreMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetBackupRestoreModeOfNextBoot(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetBasicProductShape(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetBetaUpdateTestForRcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetBioUsageAll(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetBootReqNotifyCount(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetBudget(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetBuildNumber(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetCallRecord(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetChildApp(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetCompiledSdkVersion(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetCompiledSdkVersionByPath(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetCompiledSdkVersionByPid(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetCoredumpAppInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetCpuUsage(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetCpuUsageAll(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetCpuUsageProc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetCpuUsageProc2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetCpuUsageThread(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetCpumodeGame(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetCurrentFanDuty(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetDataTransferMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetDebugMenuMiniModeForRcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetDebugMenuModeForPsmForRcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetDebugMenuModeForRcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetDefaultToolMemorySize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetEmergencyErrorLog(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetExecutableModuleHandle(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetExtLibcHandle(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetFakeFinalizeMenuForRcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetFirstImageAddr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetFlagedUpdaterForRcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetForceUpdateModeForRcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetGPO(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetHwFeatureInfoForDecid(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetHwModelName(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetHwSerialNumber(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetIdPs(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetIdTableCurrentCount(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetIoreq(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetIpcPath(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetKnobs(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetKqueueFromEqueue(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetMemoryPstate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetModuleInfoInternal(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetModuleListInternal(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetOpenPsIdForSystem(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetPagingStatsOfAllObjects(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetPagingStatsOfAllThreads(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetParentApp(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetPhysPageSize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetProcessName(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetProcessType(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetProcessTypeOfBudget(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetProductCode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetProductStr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetPs4SystemSwVersion(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetPsmIntdevModeForRcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetPsnAccessTraceLogForRcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetQafExpirationTimeNotafterForRcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetQafExpirationTimeNotbeforeForRcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetQafGenerationForRcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetQafNameForRcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetRenderingMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetResidentCount(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetResidentFmemCount(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetResource(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetResourceLimit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetResourceOne(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetSafemode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetSanitizerMallocReplace(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetSanitizerNewReplace(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetSocClock(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetSpecialIForRcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetSubsysId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetSubsysSubId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetSystemExVersion(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetSystemLevelDebuggerModeForRcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetSystemSwBeta(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetThreadName(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetTraceMemoryStats(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetUniversalMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetUpdVersion(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetUtokenDataExecutionForRcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetUtokenExpirationTimeNotafterForRcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetUtokenExpirationTimeNotbeforeForRcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetUtokenFakeSharefactoryForRcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetUtokenFlagedUpdaterForRcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetUtokenNpEnvSwitchingForRcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetUtokenSaveDataRepairForRcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetUtokenStoreModeForRcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetUtokenUseSoftwagnerForAcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetUtokenUseSoftwagnerForRcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetUtokenWeakenedPortRestrictionForRcmgr(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGetVrCaptureSize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelGiveDirectMemoryToMapper(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelHasNeoMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelHwGetBdDriveInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelHwHasOpticalOut(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelHwHasWlanBt(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccControlBDPowerState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccControlUSBPowerState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccGetBDPowerState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccGetBootTimestamp(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccGetCPMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccGetCountTime(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccGetCpuInfoBit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccGetErrLog(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccGetHwInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccGetPowerNumberOfBootShutdown(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccGetPowerOperatingTime(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccGetPowerUpCause(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccGetSysEventLog(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccGetUSBPowerState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccIndicatorBootDone(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccIndicatorShutdown(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccIndicatorStandby(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccIndicatorStandbyBoot(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccIndicatorStandbyShutdown(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccNotifyBootStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccNvsFlush(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccReadPowerBootMessage(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccSetBuzzer(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccSetCPMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccSetCpuInfoBit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIccSetDownloadMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelInternalGetKmemStatistics(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelInternalGetMapStatistics(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelInternalHeapPrintBacktraceWithModuleInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelInternalMapDirectMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelInternalMapNamedDirectMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelInternalResumeDirectMemoryRelease(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelInternalSuspendDirectMemoryRelease(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIsAllowedToSelectDvdRegion(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIsAuthenticNeo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIsCEX(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIsDebuggerAttached(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIsDevKit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIsExperimentalBeta(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIsGenuineCEX(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIsGenuineDevKit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIsGenuineKratosCex(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIsGenuineN(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIsGenuineTestKit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIsInSandbox(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIsKratos(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIsM2DeviceAttached(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIsMainOnStanbyMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIsMainOnStandbyMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIsModuleLoaded(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIsNeoMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIsProspero(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIsPs4Process(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelIsTestKit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelJitGetSharedMemoryInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelKernelHeapUsage(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelLoadStartModuleForSysmodule(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelLoadStartModuleInternalForMono(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelLwfsAllocateBlock(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelLwfsLseek(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelLwfsSetAttribute(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelLwfsTrimBlock(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelLwfsWrite(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelMapNamedSystemFlexibleMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelMapSanitizerShadowMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelMapToolMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelMapTraceMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelMapperGetParam(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelMemoryPoolMove(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelMlockall(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelMsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelNormalizePath(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelNotifyAppEvent(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelNotifyAppStateChanged(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelNotifySystemSuspendResumeProgress(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelNotifySystemSuspendStart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelOpenEport(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelOpenInternal(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelOpenSema(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelPrepareDirectMemorySwap(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelPrepareToResumeProcess(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelPrepareToSuspendProcess(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelProtectDirectMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelProtectDirectMemoryForPID(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelQueryToolMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelQueryTraceMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelRandomizedPath(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelRdup(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelReboot(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelReleaseToolMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelReleaseTraceDirectMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelReportUnpatchedFunctionCall(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelReserve2mbPage(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelReserveSystemDirectMemory(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelRestoreApp(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelResumeDirectMemoryRelease(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelResumeProcess(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelRtldControl(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSandboxPath(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSaveApp(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetAppInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetAppState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetBackupRestoreMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetBaseModeClock(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetBesteffort(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetBootReqNotify(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetBudget(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetCallRecord(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetCompressionAttribute(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetCoredumpAppInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetCpumodeGame(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetDataTransferMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetEmergencyErrorLog(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetFsstParam(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetGPI(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetGameDirectMemoryLimit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetGpuCu(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetIoreq(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetKnobs(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetMemoryPstate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetNeoModeClock(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetPackageInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetPhysFmemLimit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetProcessName(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetProcessProperty(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetProcessPropertyString(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetSafemode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetSafemodeDirect(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetSuspendState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetTimezoneInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetUpdatemode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSetVmContainer(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSettimeofday(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSflashGetWritePrio(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSflashSetWritePrio(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSlvNotifyError(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSpawn(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelStreamWriteActivate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelStreamWriteCreate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelStreamWriteDeactivate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelStreamWriteDelete(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelStreamWriteRead(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelStreamWriteStat(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelStreamWriteWrite(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSuspendDirectMemoryRelease(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSuspendProcess(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSuspendSystem(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSwitchToBaseMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSwitchToNativeMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelSwitchToNeoMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelTerminateProcess(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelTerminateSysCore(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelTitleWorkaroundIsEnabled(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelTitleWorkdaroundIsEnabled(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelTraceMemoryTypeProtect(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelTriggerEport(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelVirtualQueryAll(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelWaitCommandBufferCompletion(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelWriteEventQueueOnCompletionCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelWriteMapCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelWriteMapCommand2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelWriteMapDirectCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelWriteMapDirectWithGpuMaskIdCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelWriteMapWithGpuMaskIdCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelWriteModifyMtypeProtectWithGpuMaskIdCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelWriteModifyProtectCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelWriteModifyProtectWithGpuMaskIdCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelWriteMultiMapCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelWriteMultiMapWithGpuMaskIdCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelWriteRemapCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelWriteRemapWithGpuMaskIdCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelWriteSdkEventLog(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelWriteSdkEventLogV(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelWriteUnmapCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelWriteWaitOnAddressCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelWriteWaitOnCounterCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelWriteWriteAddressCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelWriteWriteCounterCommand(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceKernelYieldCpumode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLibcMspaceCreateForMonoMutex(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadAtfork(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadAttrGetscope(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadAttrSetcreatesuspend(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadAttrSetscope(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadBarrierDestroy(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadBarrierInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadBarrierWait(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadBarrierattrDestroy(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadBarrierattrGetpshared(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadBarrierattrInit(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadBarrierattrSetpshared(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadCondattrGetclock(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadGetconcurrency(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadGetcpuclockid(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadMain(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadMulti(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadMutexGetprioceiling(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadMutexInitForInternalLibc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadMutexIsowned(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadMutexSetprioceiling(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadMutexattrGetgen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadMutexattrGetkind(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadMutexattrInitForInternalLibc(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadMutexattrSetgen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadMutexattrSetkind(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadResume(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadResumeAll(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadSetBesteffort(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadSetDefaultstacksize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadSetName(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadSetconcurrency(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadSingle(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadSuspend(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadSuspendAll(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePthreadTimedjoin(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sched_getparam_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sched_getscheduler_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sched_rr_get_interval_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sched_setparam_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sched_setscheduler_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sem_close_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sem_open_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sem_setname_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sem_unlink_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI set_phys_fmem_limit_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI setlogin_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI settimeofday_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sigqueue_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sigreturn_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI statfs_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI swapcontext_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI swapon_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI symlinkat_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sysKernelGetIntdevModeForInternalLibc_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sysKernelGetIntdevModeForRcmgr_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sysKernelGetLowerLimitSysexVersion_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sysKernelGetLowerLimitUpdVersion_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sysKernelGetManufacturingMode_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sysKernelGetUpdVersion_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sysc_e00_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sysc_m00_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sysc_s00_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI tcgetsid_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI tcsetsid_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI unmount_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI utrace_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI uuidgen_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI vfork_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI wait3_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI wait4_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 3892553481..9c7d6fc2b6 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -139,6 +139,15 @@ std::int64_t CopyLinkTarget(const std::string& target, char* buffer, std::size_t return static_cast(count); } +bool HasNonDirectoryPrefix(const std::filesystem::path& native) { + std::error_code error; + for (auto prefix = native.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; +} + std::int64_t ReadNativeLink(const std::filesystem::path& native, char* buffer, std::size_t size) { #ifdef _WIN32 const DWORD attributes = GetFileAttributesW(native.wstring().c_str()); @@ -153,8 +162,10 @@ std::int64_t ReadNativeLink(const std::filesystem::path& native, char* buffer, s #endif std::error_code error; const auto status = std::filesystem::symlink_status(native, error); - if (error) return Fail(GuestError(error)); - if (status.type() == std::filesystem::file_type::not_found) return Fail(GuestEnoent); + if (error) + return Fail(error == std::errc::no_such_file_or_directory && HasNonDirectoryPrefix(native) ? GuestEnotdir : GuestError(error)); + if (status.type() == std::filesystem::file_type::not_found) + return Fail(HasNonDirectoryPrefix(native) ? GuestEnotdir : GuestEnoent); if (!std::filesystem::is_symlink(status)) return Fail(GuestEinval); const auto target = std::filesystem::read_symlink(native, error).generic_string(); if (error) return Fail(GuestError(error)); @@ -176,15 +187,6 @@ bool HasLongComponent(std::string_view path) { return false; } -bool HasNonDirectoryPrefix(const std::filesystem::path& native) { - std::error_code error; - for (auto prefix = native.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; -} - bool IsDotOrDotDot(std::string_view path) { while (!path.empty() && (path.back() == '/' || path.back() == '\\')) path.remove_suffix(1); const auto separator = path.find_last_of("/\\"); @@ -273,18 +275,18 @@ int APS5_VABI fstatfs_nid_postfix(int d, void* buf) { } std::int64_t APS5_VABI readlink_nid_postfix(const char* path, char* buffer, std::size_t size) { + if (size > static_cast(std::numeric_limits::max())) return Fail(GuestEinval); if (path == nullptr) return Fail(GuestEfault); if (*path == '\0') return Fail(GuestEnoent); - if (size > static_cast(std::numeric_limits::max())) return Fail(GuestEinval); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return Fail(error); return ReadNativeLink(native, buffer, size); } std::int64_t APS5_VABI readlinkat_nid_postfix(int fd, const char* path, char* buffer, std::size_t size) { + if (size > static_cast(std::numeric_limits::max())) return Fail(GuestEinval); if (path == nullptr) return Fail(GuestEfault); if (*path == '\0') return Fail(GuestEnoent); - if (size > static_cast(std::numeric_limits::max())) return Fail(GuestEinval); if (fd == GuestAtFdcwd || *path == '/') return readlink_nid_postfix(path, buffer, size); int error = 0; const char* base = GetDirectoryDescriptorPath_nid_no_patch(fd, &error); @@ -444,6 +446,7 @@ int APS5_VABI openat_nid_postfix(int fd, const char* path, int flags, ...) { va_end(arguments); #endif } + if (File::InvalidOpenFlags(flags)) return static_cast(Fail(GuestEinval)); if (path == nullptr) return static_cast(Fail(GuestEfault)); if (*path == '\0') return static_cast(Fail(GuestEnoent)); if (fd == GuestAtFdcwd || *path == '/') return open_nid_postfix(path, flags, mode); @@ -542,6 +545,19 @@ int APS5_VABI fchown_nid_postfix(int fd, std::uint32_t uid, std::uint32_t gid) { return 0; } +int APS5_VABI stat_nid_postfix(const char* path, FileStat* sb); +int APS5_VABI lstat_nid_postfix(const char* path, FileStat* sb); + +int APS5_VABI fstatat_nid_postfix(int fd, const char* path, FileStat* sb, int flag) { + if (flag & ~GuestAtSymlinkNofollow) return static_cast(Fail(GuestEinval)); + if (path == nullptr) return static_cast(Fail(GuestEfault)); + if (*path == '\0') return static_cast(Fail(GuestEnoent)); + if (fd == GuestAtFdcwd || *path == '/') return flag ? lstat_nid_postfix(path, sb) : stat_nid_postfix(path, sb); + const int result = StatInDirectory_nid_no_patch(fd, path, sb, flag == 0); + if (result < 0) return static_cast(Fail(result & 0xffff)); + return 0; +} + int APS5_VABI unlink_nid_postfix(const char* path); int APS5_VABI rmdir_nid_postfix(const char* path); diff --git a/core/libs/tests/ApplicationHeap.cpp b/core/libs/tests/ApplicationHeap.cpp index 16f537b48f..be99345d50 100644 --- a/core/libs/tests/ApplicationHeap.cpp +++ b/core/libs/tests/ApplicationHeap.cpp @@ -238,6 +238,18 @@ int main(int argc, char** argv) { require(frees == 3 && initializes == 1); return 0; } + if (argc > 1 && std::strcmp(argv[1], "sdk-default") == 0) { + require(argc == 3); + void* sdk = dlopen(argv[2], RTLD_NOW | RTLD_GLOBAL); + require(sdk != nullptr && initializes == 1); + std::memset(replacement.data() + 0x10, 0, replacement.size() - 0x10); + ApplicationHeapInitialize_nid_no_patch(process.data()); + void* pointer = ApplicationHeapAllocate_nid_no_patch(24); + require(pointer != nullptr && pointer != storage.data() && lastSize == 0); + ApplicationHeapFree_nid_no_patch(pointer); + require(frees == 0 && dlclose(sdk) == 0 && finalizes == 1); + return 0; + } #endif if (argc > 1 && std::strcmp(argv[1], "lazy") == 0) { installProcessParameters(); diff --git a/core/libs/tests/CpuTopology.cpp b/core/libs/tests/CpuTopology.cpp index 09ddd35682..3502b849dd 100644 --- a/core/libs/tests/CpuTopology.cpp +++ b/core/libs/tests/CpuTopology.cpp @@ -34,6 +34,28 @@ int main() { Require(Parse("0;1\n") == 0, "malformed separator"); Require(CpuTopology::MaskFromCpuList("/nonexistent/anyps5/cpus") == 0, "missing file"); + { + std::uint64_t byClass[4] = {}; + bool seen[4] = {}; + const auto empty = CpuTopology::LayoutFromClasses(byClass, seen, 4); + Require(!empty.hybrid && empty.efficient == 0 && empty.performant == 0, "empty classes"); + + byClass[0] = 0xff; + seen[0] = true; + const auto single = CpuTopology::LayoutFromClasses(byClass, seen, 4); + Require(!single.hybrid && single.efficient == 0 && single.performant == 0, "single class"); + + byClass[1] = 0xff00; + seen[1] = true; + const auto twoTier = CpuTopology::LayoutFromClasses(byClass, seen, 4); + Require(twoTier.hybrid && twoTier.efficient == 0xff && twoTier.performant == 0xff00, "two-tier classes"); + + byClass[2] = 0xff0000; + seen[2] = true; + const auto threeTier = CpuTopology::LayoutFromClasses(byClass, seen, 4); + Require(threeTier.hybrid && threeTier.efficient == 0xffff && threeTier.performant == 0xff0000, "three-tier classes aggregate efficient"); + } + const auto& layout = CpuTopology::Get(); Require(layout.process != 0, "process affinity read"); Require(!layout.hybrid || (layout.performant & layout.efficient) == 0, "hybrid classes overlap"); diff --git a/core/libs/tests/ExceptionRuntime.cpp b/core/libs/tests/ExceptionRuntime.cpp index b3d3273308..54111f196c 100644 --- a/core/libs/tests/ExceptionRuntime.cpp +++ b/core/libs/tests/ExceptionRuntime.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -31,6 +32,7 @@ extern "C" [[noreturn]] void _ZNSt8__sce_v219_Xbad_function_callEv_nid_postfix() extern "C" [[noreturn]] void __cxa_bad_cast_nid_postfix(); extern "C" [[noreturn]] void _ZSt19_Throw_bad_weak_ptrv_nid_postfix(); extern "C" [[noreturn]] void _ZSt26__throw_bad_variant_accessv_nid_postfix(); +extern "C" [[noreturn]] void _ZSt26_Throw_bad_optional_accessv_nid_postfix(); extern "C" [[noreturn]] void _ZNKSt9exception6_RaiseEv_nid_postfix(const void*); extern "C" void* __cxa_vec_new3_nid_postfix(std::size_t, std::size_t, std::size_t, void(*)(void*), void(*)(void*), void*(*)(std::size_t), void(*)(void*, std::size_t)); extern "C" void __cxa_vec_delete3_nid_postfix(void*, std::size_t, std::size_t, void(*)(void*), void(*)(void*, std::size_t)); @@ -254,6 +256,8 @@ int main() { catch (const std::bad_weak_ptr& value) { if (std::strcmp(value.what(), "bad_weak_ptr") != 0) std::abort(); } try { _ZSt26__throw_bad_variant_accessv_nid_postfix(); std::abort(); } catch (const std::bad_variant_access& value) { if (std::strcmp(value.what(), "bad_variant_access") != 0) std::abort(); } + try { _ZSt26_Throw_bad_optional_accessv_nid_postfix(); std::abort(); } + catch (const std::bad_optional_access& value) { if (std::strcmp(value.what(), "bad_optional_access") != 0) std::abort(); } try { _ZNKSt9exception6_RaiseEv_nid_postfix(nullptr); assert(false); } catch (const std::invalid_argument&) {} try { _ZNKSt9exception6_RaiseEv_nid_postfix(&object); assert(false); } diff --git a/core/libs/tests/FileStreams.cpp b/core/libs/tests/FileStreams.cpp index 0a4742b900..f53419748c 100644 --- a/core/libs/tests/FileStreams.cpp +++ b/core/libs/tests/FileStreams.cpp @@ -9,7 +9,9 @@ #include #include #include +#include #include +#include #include #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN @@ -33,7 +35,13 @@ int APS5_VABI fflush_nid_postfix(FileStream* stream); int APS5_VABI remove_nid_postfix(const char* path); int APS5_VABI rename_nid_postfix(const char* from, const char* to); int APS5_VABI ferror_nid_postfix(FileStream* stream); +int APS5_VABI feof_nid_postfix(FileStream* stream); void APS5_VABI clearerr_nid_postfix(FileStream* stream); +int APS5_VABI fgetc_nid_postfix(FileStream* stream); +int APS5_VABI getc_unlocked_nid_postfix(FileStream* stream); +char* APS5_VABI fgets_nid_postfix(char* buffer, int size, FileStream* stream); +int APS5_VABI ungetc_nid_postfix(int value, FileStream* stream); +int APS5_VABI fscanf_nid_postfix(FileStream* stream, const char* format, ...); } static void Require(bool condition, const char* message = "File stream check failed") { @@ -115,9 +123,83 @@ static void CheckExclusiveCppOpen(const std::string& base) { } } +static std::string ReadWhole(const std::string& path) { + auto* stream = fopen_nid_postfix(path.c_str(), "rb"); + Require(stream != nullptr); + std::array buffer{}; + const auto size = fread_nid_postfix(buffer.data(), 1, buffer.size(), stream); + Require(fclose_nid_postfix(stream) == 0); + return std::string(buffer.data(), size); +} + +template +static void CheckReadAfterWrite(const std::string& path, const char* mode, const char* expected, TRead read) { + auto* stream = fopen_nid_postfix(path.c_str(), "wb"); + Require(stream != nullptr && fputs_nid_postfix("0123456789", stream) >= 0 && fclose_nid_postfix(stream) == 0); + stream = fopen_nid_postfix(path.c_str(), mode); + Require(stream != nullptr && fwrite_nid_postfix("AB", 1, 2, stream) == 2); + read(stream); + Require(ferror_nid_postfix(stream) == 0, "Read after write on an update stream set the error indicator"); + Require(fclose_nid_postfix(stream) == 0); + Require(ReadWhole(path) == expected, "Read after write on an update stream changed the file"); +} + +static void CheckUpdateStreamReadsAfterWrites(const std::string& base) { + const std::string path = base + ".update"; + constexpr char overwritten[] = "AB23456789"; + CheckReadAfterWrite(path, "r+b", overwritten, [](FileStream* stream) { + std::array buffer{}; + Require(fread_nid_postfix(buffer.data(), 1, 3, stream) == 3, "fread after fwrite on an r+ stream"); + Require(std::strcmp(buffer.data(), "234") == 0, "fread after fwrite on an r+ stream read other bytes"); + Require(ftell_nid_postfix(stream) == 5); + }); + CheckReadAfterWrite(path, "r+b", overwritten, [](FileStream* stream) { + std::vector buffer(65536); + Require(fread_nid_postfix(buffer.data(), 1, buffer.size(), stream) == 8, "fread of more than the stream buffer after fwrite on an r+ stream"); + Require(std::string_view(buffer.data(), 8) == "23456789", "fread of more than the stream buffer after fwrite on an r+ stream read other bytes"); + Require(ftell_nid_postfix(stream) == 10); + }); + CheckReadAfterWrite(path, "r+b", overwritten, [](FileStream* stream) { + Require(fgetc_nid_postfix(stream) == '2', "fgetc after fwrite on an r+ stream"); + }); + CheckReadAfterWrite(path, "r+b", overwritten, [](FileStream* stream) { + Require(getc_unlocked_nid_postfix(stream) == '2', "getc_unlocked after fwrite on an r+ stream"); + }); + CheckReadAfterWrite(path, "r+b", overwritten, [](FileStream* stream) { + std::array buffer{}; + Require(fgets_nid_postfix(buffer.data(), static_cast(buffer.size()), stream) == buffer.data(), "fgets after fwrite on an r+ stream"); + Require(std::strcmp(buffer.data(), "234") == 0, "fgets after fwrite on an r+ stream read other bytes"); + }); + CheckReadAfterWrite(path, "r+b", overwritten, [](FileStream* stream) { + Require(ungetc_nid_postfix('Z', stream) == 'Z', "ungetc after fwrite on an r+ stream"); + Require(fgetc_nid_postfix(stream) == 'Z' && fgetc_nid_postfix(stream) == '2', "ungetc after fwrite on an r+ stream pushed back other bytes"); + }); + CheckReadAfterWrite(path, "r+b", overwritten, [](FileStream* stream) { + std::array buffer{}; + Require(fscanf_nid_postfix(stream, "%3s", buffer.data()) == 1, "fscanf after fwrite on an r+ stream"); + Require(std::strcmp(buffer.data(), "234") == 0, "fscanf after fwrite on an r+ stream read other bytes"); + }); + CheckReadAfterWrite(path, "w+b", "AB", [](FileStream* stream) { + std::array buffer{}; + Require(fread_nid_postfix(buffer.data(), 1, buffer.size(), stream) == 0 && feof_nid_postfix(stream) != 0, "fread after fwrite on a w+ stream"); + }); + CheckReadAfterWrite(path, "a+b", "0123456789AB", [](FileStream* stream) { + Require(fgetc_nid_postfix(stream) == EOF && feof_nid_postfix(stream) != 0, "fgetc after fwrite on an a+ stream"); + }); + auto* stream = fopen_nid_postfix(path.c_str(), "r+b"); + Require(stream != nullptr); + Require(ungetc_nid_postfix('Z', stream) == 'Z', "ungetc on a new r+ stream"); + Require(fgetc_nid_postfix(stream) == 'Z' && fgetc_nid_postfix(stream) == '0', "ungetc on a new r+ stream pushed back other bytes"); + Require(fseek_nid_postfix(stream, 2, SEEK_SET) == 0 && ungetc_nid_postfix('Y', stream) == 'Y', "ungetc after fseek on an r+ stream"); + Require(fgetc_nid_postfix(stream) == 'Y' && fgetc_nid_postfix(stream) == '2', "ungetc after fseek on an r+ stream pushed back other bytes"); + Require(fclose_nid_postfix(stream) == 0); + Require(std::remove(path.c_str()) == 0); +} + int main(int argc, char** argv) { Require(argc == 2); CheckExclusiveCppOpen(argv[1]); + CheckUpdateStreamReadsAfterWrites(argv[1]); Require(_Stdout_nid_postfix.GetHandle() == stdout); Require(_Stderr_nid_postfix.GetHandle() == stderr); Require(fputs_nid_postfix("stdout object works\n", &_Stdout_nid_postfix) >= 0); @@ -206,6 +288,17 @@ int main(int argc, char** argv) { std::array appended{}; Require(stream != nullptr && fread_nid_postfix(appended.data(), 1, 4, stream) == 4 && std::strcmp(appended.data(), "abcd") == 0); Require(fclose_nid_postfix(stream) == 0 && remove_nid_postfix(heldName.c_str()) == 0); + stream = fopen_nid_postfix(heldName.c_str(), "w+b"); + Require(stream != nullptr); + stream = freopen_nid_postfix(movedName.c_str(), "w+b", stream); + Require(stream != nullptr && fwrite_nid_postfix(payload, 1, sizeof(payload), stream) == sizeof(payload)); + Require(fflush_nid_postfix(stream) == 0); + Require(rename_nid_postfix(movedName.c_str(), heldName.c_str()) == 0 && !std::filesystem::exists(movedName)); + Require(remove_nid_postfix(heldName.c_str()) == 0 && !std::filesystem::exists(heldName)); + Require(fseek_nid_postfix(stream, 0, SEEK_SET) == 0); + std::array reopenedBuffer{}; + Require(fread_nid_postfix(reopenedBuffer.data(), 1, reopenedBuffer.size(), stream) == reopenedBuffer.size()); + Require(std::strcmp(reopenedBuffer.data(), payload) == 0 && fclose_nid_postfix(stream) == 0); constexpr char marker[] = "marker"; stream = fopen_nid_postfix(argv[1], "wb"); Require(stream != nullptr); diff --git a/core/libs/tests/Formatting.cpp b/core/libs/tests/Formatting.cpp index f7eae1cd6a..f22d7880dd 100644 --- a/core/libs/tests/Formatting.cpp +++ b/core/libs/tests/Formatting.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -281,17 +282,50 @@ __attribute__((noinline)) static void APS5_VABI RunChecks() { Require(sscanf_nid_postfix(" ", "%*d%d", &scanned) == EOF); Require(sscanf_nid_postfix("", "%d", &scanned) == EOF); #else - char16_t wide[4] = {u'!', u'!', u'!', u'!'}; - const auto rejects = [&](const char* format) { - try { sscanf_nid_postfix("ab cd", format, wide); } catch (const std::runtime_error&) { return wide[0] == u'!'; } - return false; - }; - Require(rejects("%ls") && rejects("%*s %3lc") && rejects("%l[a-z]") && rejects("%S") && rejects("%2$C") && rejects("%'ls")); - const auto secureRejects = [&](const char* format) { - try { ScanListS("ab cd", format, wide, 4u); } catch (const std::runtime_error&) { return wide[0] == u'!'; } - return false; - }; - Require(secureRejects("%ls") && secureRejects("%lc")); + { + char16_t first[8] = {}; + char16_t letter = u'!'; + Require(sscanf_nid_postfix("ab cd", "%ls %lc", first, &letter) == 2); + Require(std::u16string(first) == u"ab" && letter == u'c'); + Require(sscanf_nid_postfix(" xy z", "%S %C", first, &letter) == 2 && std::u16string(first) == u"xy" && letter == u'z'); + char16_t three[5] = {u'!', u'!', u'!', u'!', u'!'}; + Require(sscanf_nid_postfix("abcdef", "%3lc", three) == 1); + Require(three[0] == u'a' && three[1] == u'b' && three[2] == u'c' && three[3] == u'!'); + Require(sscanf_nid_postfix("abcz", "%l[a-c]", first) == 1 && std::u16string(first) == u"abc"); + Require(sscanf_nid_postfix("ab cd", "%l[^ ]", first) == 1 && std::u16string(first) == u"ab"); + Require(sscanf_nid_postfix("]x", "%l[]x]", first) == 1 && std::u16string(first) == u"]x"); + char16_t second[8] = {}; + Require(sscanf_nid_postfix("\xc5\xbelu\xc5\xa5 k\xc5\xaf\xc5\x88", "%ls %ls", first, second) == 2); + Require(std::u16string(first) == u"\u017elu\u0165" && std::u16string(second) == u"k\u016f\u0148"); + int number = 0; + Require(sscanf_nid_postfix("\xc5\xbelu\xc5\xa5 7", "%2ls%*ls %d", first, &number) == 2); + Require(std::u16string(first) == u"\u017el" && number == 7); + char narrowWord[8] = {}; + Require(sscanf_nid_postfix("12 ab cd", "%d %ls %7s", &number, first, narrowWord) == 3); + Require(number == 12 && std::u16string(first) == u"ab" && std::strcmp(narrowWord, "cd") == 0); + int consumed = -1; + Require(sscanf_nid_postfix("ab cd", "%ls%n", first, &consumed) == 1 && consumed == 2); + Require(sscanf_nid_postfix("", "%ls", first) == EOF && sscanf_nid_postfix(" ", "%ls", first) == EOF); + Require(sscanf_nid_postfix("xyz", "%l[a-c]", first) == 0); + Require(sscanf_nid_postfix("1 xyz", "%d %l[a-c]", &number, first) == 1); + Require(ScanList("hi there", "%ls %lc", first, &letter) == 2 && std::u16string(first) == u"hi" && letter == u't'); + char16_t fits[6] = {}; + Require(ScanListS("hello world", "%ls", fits, 6u) == 1 && std::u16string(fits) == u"hello"); + char16_t small[5] = {u'!', u'!', u'!', u'!', u'!'}; + Require(ScanListS("hello", "%ls", small, 5u) == 0 && small[0] == 0); + Require(ScanListS("hello", "%4ls", small, 5u) == 1 && std::u16string(small) == u"hell"); + Require(ScanListS("xy", "%lc", &letter, 1u) == 1 && letter == u'x'); + small[0] = u'!'; + Require(ScanListS("xyz", "%3lc", small, 2u) == 0 && small[0] == 0); + Require(ScanListS("ab cd", "%*ls %l[a-z]", fits, 6u) == 1 && std::u16string(fits) == u"cd"); + char16_t untouched[4] = {u'!', u'!', u'!', u'!'}; + const auto rejects = [&](const char* input, const char* format) { + try { sscanf_nid_postfix(input, format, untouched); } catch (const std::runtime_error&) { return untouched[0] == u'!'; } + return false; + }; + Require(rejects("\xff", "%ls") && rejects("a b", "%2$ls") && rejects("\xc5\xbe", "%l[\xc5\xbe]")); + Require(rejects("\xf0\x9f\x98\x80", "%ls")); + } char narrow[8] = {}; Require(sscanf_nid_postfix("ab%", "%7[^%ls]", narrow) == 1 && std::strcmp(narrow, "ab") == 0); #endif diff --git a/core/libs/tests/FreeTypeFontFixture.hpp b/core/libs/tests/FreeTypeFontFixture.hpp new file mode 100644 index 0000000000..9458d09a81 --- /dev/null +++ b/core/libs/tests/FreeTypeFontFixture.hpp @@ -0,0 +1,76 @@ +#ifndef CORE_LIBS_TESTS_FREETYPEFONTFIXTURE_HPP +#define CORE_LIBS_TESTS_FREETYPEFONTFIXTURE_HPP + +#include +#include +#include +#include + +static void Put16(std::vector& out, std::uint32_t value) { + out.push_back(static_cast(value >> 8)); + out.push_back(static_cast(value)); +} + +static void Put32(std::vector& out, std::uint32_t value) { + Put16(out, value >> 16); + Put16(out, value & 0xFFFFu); +} + +static void PutAll16(std::vector& out, std::initializer_list values) { + for (const int value : values) Put16(out, static_cast(static_cast(value))); +} + +static std::vector SquareGlyphFont(std::uint32_t codepoint, int width, bool symbolMap = false) { + std::vector head; + Put32(head, 0x00010000u); + Put32(head, 0x00010000u); + Put32(head, 0); + Put32(head, 0x5F0F3CF5u); + PutAll16(head, {0x000B, 1000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, width, 800, 0, 8, 2, 0, 0}); + std::vector hhea; + Put32(hhea, 0x00010000u); + PutAll16(hhea, {800, -200, 0, width, 0, 0, width, 1, 0, 0, 0, 0, 0, 0, 0, 2}); + std::vector maxp; + Put32(maxp, 0x00010000u); + PutAll16(maxp, {2, 4, 1, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0}); + std::vector hmtx; + PutAll16(hmtx, {width, 0, width, 100}); + std::vector glyf; + PutAll16(glyf, {1, 100, 0, width - 100, 700, 3, 0}); + glyf.insert(glyf.end(), {1, 1, 1, 1}); + PutAll16(glyf, {100, 0, width - 200, 0, 0, 700, 0, -700}); + std::vector loca; + PutAll16(loca, {0, 0, static_cast(glyf.size() / 2)}); + std::vector cmap; + PutAll16(cmap, {0, symbolMap ? 2 : 1, 3, 1}); + Put32(cmap, symbolMap ? 20 : 12); + if (symbolMap) { + PutAll16(cmap, {3, 0}); + Put32(cmap, 52); + } + PutAll16(cmap, {4, 32, 0, 4, 4, 1, 0, static_cast(codepoint), 0xFFFF, 0, static_cast(codepoint), 0xFFFF, 1 - static_cast(codepoint), 1, 0, 0}); + if (symbolMap) { + const int symbol = static_cast(codepoint + 0xF000); + PutAll16(cmap, {4, 32, 0, 4, 4, 1, 0, symbol, 0xFFFF, 0, symbol, 0xFFFF, 1 - symbol, 1, 0, 0}); + } + const std::vector*>> tables = { + {"cmap", &cmap}, {"glyf", &glyf}, {"head", &head}, {"hhea", &hhea}, {"hmtx", &hmtx}, {"loca", &loca}, {"maxp", &maxp}}; + std::vector font; + Put32(font, 0x00010000u); + PutAll16(font, {static_cast(tables.size()), 64, 2, static_cast(tables.size()) * 16 - 64}); + std::uint32_t offset = 12 + static_cast(tables.size()) * 16; + for (const auto& [tag, data] : tables) { + font.insert(font.end(), tag, tag + 4); + Put32(font, 0); + Put32(font, offset); + Put32(font, static_cast(data->size())); + offset += (static_cast(data->size()) + 3u) & ~3u; + } + for (const auto& [tag, data] : tables) { + font.insert(font.end(), data->begin(), data->end()); + font.resize((font.size() + 3u) & ~std::size_t{3}); + } + return font; +} + +#endif 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/GuestAddressConversion.cpp b/core/libs/tests/GuestAddressConversion.cpp index 675da4ed60..4d44b0a41b 100644 --- a/core/libs/tests/GuestAddressConversion.cpp +++ b/core/libs/tests/GuestAddressConversion.cpp @@ -3,14 +3,579 @@ #include #include #include +#include extern "C" { int APS5_VABI __inet_pton_nid_postfix(int, const char*, void*); const char* APS5_VABI __inet_ntop_nid_postfix(int, const void*, char*, std::uint32_t); +int APS5_VABI inet_pton_nid_postfix(int, const char*, void*); +const char* APS5_VABI inet_ntop_nid_postfix(int, const void*, char*, std::uint32_t); int* APS5_VABI __error_nid_postfix(); +std::uint32_t APS5_VABI htonl_nid_postfix(std::uint32_t); +std::uint32_t APS5_VABI ntohl_nid_postfix(std::uint32_t); +std::uint16_t APS5_VABI htons_nid_postfix(std::uint16_t); +std::uint16_t APS5_VABI ntohs_nid_postfix(std::uint16_t); } static void Require(bool value) { if (!value) std::abort(); } + +using Ntop = const char* (APS5_VABI *)(int, const void*, char*, std::uint32_t); + +struct PtonCase { bool plain; int family; const char* text; int result; int error; const char* bytes; }; +struct NtopCase { bool plain; int family; const char* source; std::uint32_t size; bool succeeds; int error; const char* bytes; std::size_t lastTouched; }; +struct PadCase { int family; const char* source; std::uint32_t size; std::size_t lastTouched; }; + +static const PtonCase ConsolePton[] = { + {false, 2, "1.2.3.4", 1, 0, "01020304aaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "0.0.0.0", 1, 0, "00000000aaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "255.255.255.255", 1, 0, "ffffffffaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "256.1.1.1", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "01.2.3.4", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "1.2.3.04", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "001.002.003.004", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "1.2.3.0", 1, 0, "01020300aaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "1.2.3", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "127.1", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "1.2.3.4.5", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "1..2.3", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, " 1.2.3.4", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "1.2.3.4 ", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "0x1.2.3.4", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "1.2.3.-1", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "1.2.3.4/24", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "::1", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "::", 1, 0, "00000000000000000000000000000000"}, + {false, 28, "::1", 1, 0, "00000000000000000000000000000001"}, + {false, 28, "1::", 1, 0, "00010000000000000000000000000000"}, + {false, 28, "1:2:3:4:5:6:7:8", 1, 0, "00010002000300040005000600070008"}, + {false, 28, "1:2:3:4:5:6:7::", 1, 0, "00010002000300040005000600070000"}, + {false, 28, "::2:3:4:5:6:7:8", 1, 0, "00000002000300040005000600070008"}, + {false, 28, "1::2:3:4:5:6:7", 1, 0, "00010000000200030004000500060007"}, + {false, 28, "1:2:3:4:5:6:7:8:9", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "1::2::3", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "::ffff:1.2.3.4", 1, 0, "00000000000000000000ffff01020304"}, + {false, 28, "::1.2.3.4", 1, 0, "00000000000000000000000001020304"}, + {false, 28, "1:2:3:4:5:6:1.2.3.4", 1, 0, "00010002000300040005000601020304"}, + {false, 28, "1:2:3:4:5:6:7:1.2.3.4", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "::1.2.3", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "::01.2.3.4", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "fe80::1%eth0", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "FE80::ABCD", 1, 0, "fe80000000000000000000000000abcd"}, + {false, 28, "12345::", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "00001::", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "1:2:3:4:5:6:7:8:", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, ":1:2:3:4:5:6:7", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "0:0:0:0:0:0:0:0", 1, 0, "00000000000000000000000000000000"}, + {false, 28, "1.2.3.4", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, ":", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, ":::", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 9999, "1.2.3.4", -1, 47, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 0, "1.2.3.4", -1, 47, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "192.0.2.17", 1, 0, "c0000211aaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "not-an-address", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "256.1.2.3", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "01.2.3.4", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "1.2.3", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "1.2.3.4.5", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "1.2.3.4.", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, " 1.2.3.4", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "0x1.2.3.4", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "1..3.4", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "1.2.3.-4", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 2, "255.0.7.100", 1, 0, "ff000764aaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "2001:db8::17", 1, 0, "20010db8000000000000000000000017"}, + {false, 28, "not-an-address", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "::ffff:192.0.2.17", 1, 0, "00000000000000000000ffffc0000211"}, + {false, 28, "::ffff:01.2.3.4", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "::ffff:1.2.3.04", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "1:2:3:4:5:6:7:8::", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "::1:2:3:4:5:6:7:8", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "1::2::3", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, ":::", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, ":1::", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "1::2:", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, ":1:2:3:4:5:6:7", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "1:2:3:4:5:6:7", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "1:2:3:4:5:6:7:8:9", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "12345::", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "fe80::1%eth0", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "1.2.3.4::", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "::1.2.3.4:1", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "::g", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "1:2:3:4:5:6:1.2.3.4:7", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "1:2:3:4:5:6:7:1.2.3.4", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 28, "1:2:3:4:5:6:7::", 1, 0, "00010002000300040005000600070000"}, + {false, 28, "ABCD:ef01::1", 1, 0, "abcdef01000000000000000000000001"}, + {false, 28, "::ffff:0.0.0.1", 1, 0, "00000000000000000000ffff00000001"}, + {false, 28, "::ffff:0:1:0", 1, 0, "0000000000000000ffff000000010000"}, + {false, 28, "::1", 1, 0, "00000000000000000000000000000001"}, + {false, 28, "::", 1, 0, "00000000000000000000000000000000"}, + {false, 28, "1::", 1, 0, "00010000000000000000000000000000"}, + {false, 28, "::2", 1, 0, "00000000000000000000000000000002"}, + {false, 28, "::1.2.3.4", 1, 0, "00000000000000000000000001020304"}, + {false, 28, "1:0:0:2:0:0:0:3", 1, 0, "00010000000000020000000000000003"}, + {false, 28, "1:0:2:0:3:0:4:0", 1, 0, "00010000000200000003000000040000"}, + {false, 28, "0:0:1:0:0:2:0:0", 1, 0, "00000000000100000000000200000000"}, + {false, 28, "::ffff:1.2.3.4", 1, 0, "00000000000000000000ffff01020304"}, + {false, 28, "2001:DB8::17", 1, 0, "20010db8000000000000000000000017"}, + {false, 28, "0:0:0:0:0:0:1:0", 1, 0, "00000000000000000000000000010000"}, + {false, 28, "::1:0", 1, 0, "00000000000000000000000000010000"}, + {false, 28, "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", 1, 0, "ffffffffffffffffffffffffffffffff"}, + {false, 10, "::1", -1, 47, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {false, 9999, "1.2.3.4", -1, 47, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 2, "192.0.2.17", 1, 0, "c0000211aaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 2, "not-an-address", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 2, "256.1.2.3", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 2, "01.2.3.4", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 2, "1.2.3", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 2, "1.2.3.4.5", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 2, "1.2.3.4.", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 2, " 1.2.3.4", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 2, "0x1.2.3.4", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 2, "1..3.4", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 2, "1.2.3.-4", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 2, "255.0.7.100", 1, 0, "ff000764aaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 28, "2001:db8::17", 1, 0, "20010db8000000000000000000000017"}, + {true, 28, "not-an-address", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 28, "::ffff:192.0.2.17", 1, 0, "00000000000000000000ffffc0000211"}, + {true, 28, "::ffff:01.2.3.4", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 28, "::ffff:1.2.3.04", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 28, "1:2:3:4:5:6:7:8::", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 28, "::1:2:3:4:5:6:7:8", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 28, "1::2::3", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 28, ":::", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 28, ":1::", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 28, "1::2:", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 28, ":1:2:3:4:5:6:7", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 28, "1:2:3:4:5:6:7", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 28, "1:2:3:4:5:6:7:8:9", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 28, "12345::", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 28, "fe80::1%eth0", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 28, "1.2.3.4::", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 28, "::1.2.3.4:1", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 28, "::g", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 28, "1:2:3:4:5:6:1.2.3.4:7", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 28, "1:2:3:4:5:6:7:1.2.3.4", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 28, "", 0, 0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 28, "1:2:3:4:5:6:7::", 1, 0, "00010002000300040005000600070000"}, + {true, 28, "ABCD:ef01::1", 1, 0, "abcdef01000000000000000000000001"}, + {true, 28, "::ffff:0.0.0.1", 1, 0, "00000000000000000000ffff00000001"}, + {true, 28, "::ffff:0:1:0", 1, 0, "0000000000000000ffff000000010000"}, + {true, 28, "::1", 1, 0, "00000000000000000000000000000001"}, + {true, 28, "::", 1, 0, "00000000000000000000000000000000"}, + {true, 28, "1::", 1, 0, "00010000000000000000000000000000"}, + {true, 28, "::2", 1, 0, "00000000000000000000000000000002"}, + {true, 28, "::1.2.3.4", 1, 0, "00000000000000000000000001020304"}, + {true, 28, "1:0:0:2:0:0:0:3", 1, 0, "00010000000000020000000000000003"}, + {true, 28, "1:0:2:0:3:0:4:0", 1, 0, "00010000000200000003000000040000"}, + {true, 28, "0:0:1:0:0:2:0:0", 1, 0, "00000000000100000000000200000000"}, + {true, 28, "::ffff:1.2.3.4", 1, 0, "00000000000000000000ffff01020304"}, + {true, 28, "2001:DB8::17", 1, 0, "20010db8000000000000000000000017"}, + {true, 28, "0:0:0:0:0:0:1:0", 1, 0, "00000000000000000000000000010000"}, + {true, 28, "::1:0", 1, 0, "00000000000000000000000000010000"}, + {true, 28, "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", 1, 0, "ffffffffffffffffffffffffffffffff"}, + {true, 10, "::1", -1, 47, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {true, 9999, "1.2.3.4", -1, 47, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, +}; +static const NtopCase ConsoleNtop[] = { + {false, 2, "01020304", 16, true, 0, "312e322e332e34005a5a5a5a5a5a5a5a", 0}, + {false, 2, "00000000", 16, true, 0, "302e302e302e30005a5a5a5a5a5a5a5a", 0}, + {false, 2, "ffffffff", 16, true, 0, "3235352e3235352e3235352e32353500", 0}, + {false, 2, "01020304", 8, true, 0, "312e322e332e3400", 0}, + {false, 2, "01020304", 7, false, 28, "5a5a5a5a5a5a5a", 0}, + {false, 2, "01020304", 0, false, 28, "", 0}, + {false, 28, "00000000000000000000000000000000", 46, true, 0, "3a3a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 0}, + {false, 28, "00000000000000000000000000000001", 46, true, 0, "3a3a3100000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 0}, + {false, 28, "00010000000000000000000000000000", 46, true, 0, "313a3a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 0}, + {false, 28, "00010000000000020000000000000003", 46, true, 0, "313a303a303a323a3a33000000000000000000000000000000000000000000000000000000000000000000000000", 0}, + {false, 28, "00010000000000020000000000030004", 46, true, 0, "313a3a323a303a303a333a3400000000000000000000000000000000000000000000000000000000000000000000", 0}, + {false, 28, "00010000000200030004000500060007", 46, true, 0, "313a303a323a333a343a353a363a3700000000000000000000000000000000000000000000000000000000000000", 0}, + {false, 28, "00000000000000000000ffff01020304", 46, true, 0, "3a3a666666663a312e322e332e340000000000000000000000000000000000000000000000000000000000000000", 0}, + {false, 28, "00000000000000000000000001020304", 46, true, 0, "3a3a312e322e332e3400000000000000000000000000000000000000000000000000000000000000000000000000", 0}, + {false, 28, "00000000000000000000000000000002", 46, true, 0, "3a3a3200000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 0}, + {false, 28, "00000000000000000000ffff00000000", 46, true, 0, "3a3a666666663a302e302e302e300000000000000000000000000000000000000000000000000000000000000000", 0}, + {false, 28, "0000000000000000ffff000001020304", 46, true, 0, "3a3a666666663a303a3130323a333034000000000000000000000000000000000000000000000000000000000000", 0}, + {false, 28, "abcd0000000000000000000000000000", 46, true, 0, "616263643a3a00000000000000000000000000000000000000000000000000000000000000000000000000000000", 0}, + {false, 28, "00000000000000000000000000000001", 4, true, 0, "3a3a3100", 0}, + {false, 28, "00000000000000000000000000000001", 3, false, 28, "5a5a5a", 0}, + {false, 28, "ffffffffffffffffffffffffffffffff", 46, true, 0, "666666663a666666663a666666663a666666663a666666663a666666663a666666663a6666666600000000000000", 0}, + {false, 28, "ffffffffffffffffffffffffffffffff", 40, true, 0, "666666663a666666663a666666663a666666663a666666663a666666663a666666663a6666666600", 0}, + {false, 28, "ffffffffffffffffffffffffffffffff", 39, false, 28, "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a", 0}, + {false, 28, "00000000000000000000ffffffffffff", 46, true, 0, "3a3a666666663a3235352e3235352e3235352e323535000000000000000000000000000000000000000000000000", 0}, + {false, 9999, "01020304000000000000000000000000", 16, false, 47, "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a", 0}, + {false, 2, "c0000211", 64, true, 0, "3139322e302e322e313700", 11}, + {false, 2, "c0000211", 11, true, 0, "3139322e302e322e313700", 11}, + {false, 2, "c0000211", 10, false, 28, "", 0}, + {false, 2, "c0000211", 1, false, 28, "", 0}, + {false, 2, "ff000764", 64, true, 0, "3235352e302e372e31303000", 12}, + {false, 2, "ff000764", 12, true, 0, "3235352e302e372e31303000", 12}, + {false, 2, "ff000764", 11, false, 28, "", 0}, + {false, 2, "ff000764", 1, false, 28, "", 0}, + {false, 28, "20010db8000000000000000000000017", 64, true, 0, "323030313a6462383a3a313700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {false, 28, "20010db8000000000000000000000017", 13, true, 0, "323030313a6462383a3a313700", 13}, + {false, 28, "20010db8000000000000000000000017", 12, false, 28, "", 0}, + {false, 28, "20010db8000000000000000000000017", 1, false, 28, "", 0}, + {false, 28, "00000000000000000000ffffc0000211", 64, true, 0, "3a3a666666663a3139322e302e322e31370000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {false, 28, "00000000000000000000ffffc0000211", 18, true, 0, "3a3a666666663a3139322e302e322e313700", 18}, + {false, 28, "00000000000000000000ffffc0000211", 17, false, 28, "", 0}, + {false, 28, "00000000000000000000ffffc0000211", 1, false, 28, "", 0}, + {false, 28, "00010002000300040005000600070000", 64, true, 0, "313a323a333a343a353a363a373a3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {false, 28, "00010002000300040005000600070000", 16, true, 0, "313a323a333a343a353a363a373a3000", 16}, + {false, 28, "00010002000300040005000600070000", 15, false, 28, "", 0}, + {false, 28, "00010002000300040005000600070000", 1, false, 28, "", 0}, + {false, 28, "abcdef01000000000000000000000001", 64, true, 0, "616263643a656630313a3a3100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {false, 28, "abcdef01000000000000000000000001", 13, true, 0, "616263643a656630313a3a3100", 13}, + {false, 28, "abcdef01000000000000000000000001", 12, false, 28, "", 0}, + {false, 28, "abcdef01000000000000000000000001", 1, false, 28, "", 0}, + {false, 28, "00000000000000000000ffff00000001", 64, true, 0, "3a3a666666663a302e302e302e310000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {false, 28, "00000000000000000000ffff00000001", 15, true, 0, "3a3a666666663a302e302e302e3100", 15}, + {false, 28, "00000000000000000000ffff00000001", 14, false, 28, "", 0}, + {false, 28, "00000000000000000000ffff00000001", 1, false, 28, "", 0}, + {false, 28, "0000000000000000ffff000000010000", 64, true, 0, "3a3a666666663a303a313a3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {false, 28, "0000000000000000ffff000000010000", 13, true, 0, "3a3a666666663a303a313a3000", 13}, + {false, 28, "0000000000000000ffff000000010000", 12, false, 28, "", 0}, + {false, 28, "0000000000000000ffff000000010000", 1, false, 28, "", 0}, + {false, 28, "00000000000000000000000000000001", 64, true, 0, "3a3a3100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {false, 28, "00000000000000000000000000000001", 4, true, 0, "3a3a3100", 4}, + {false, 28, "00000000000000000000000000000001", 3, false, 28, "", 0}, + {false, 28, "00000000000000000000000000000001", 1, false, 28, "", 0}, + {false, 28, "00000000000000000000000000000000", 64, true, 0, "3a3a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {false, 28, "00000000000000000000000000000000", 3, true, 0, "3a3a00", 3}, + {false, 28, "00000000000000000000000000000000", 2, false, 28, "", 0}, + {false, 28, "00000000000000000000000000000000", 1, false, 28, "", 0}, + {false, 28, "00010000000000000000000000000000", 64, true, 0, "313a3a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {false, 28, "00010000000000000000000000000000", 4, true, 0, "313a3a00", 4}, + {false, 28, "00010000000000000000000000000000", 3, false, 28, "", 0}, + {false, 28, "00010000000000000000000000000000", 1, false, 28, "", 0}, + {false, 28, "00000000000000000000000000000002", 64, true, 0, "3a3a3200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {false, 28, "00000000000000000000000000000002", 4, true, 0, "3a3a3200", 4}, + {false, 28, "00000000000000000000000000000002", 3, false, 28, "", 0}, + {false, 28, "00000000000000000000000000000002", 1, false, 28, "", 0}, + {false, 28, "00000000000000000000000001020304", 64, true, 0, "3a3a312e322e332e3400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {false, 28, "00000000000000000000000001020304", 10, true, 0, "3a3a312e322e332e3400", 10}, + {false, 28, "00000000000000000000000001020304", 9, false, 28, "", 0}, + {false, 28, "00000000000000000000000001020304", 1, false, 28, "", 0}, + {false, 28, "00010000000000020000000000000003", 64, true, 0, "313a303a303a323a3a33000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {false, 28, "00010000000000020000000000000003", 11, true, 0, "313a303a303a323a3a3300", 11}, + {false, 28, "00010000000000020000000000000003", 10, false, 28, "", 0}, + {false, 28, "00010000000000020000000000000003", 1, false, 28, "", 0}, + {false, 28, "00010000000200000003000000040000", 64, true, 0, "313a303a323a303a333a303a343a3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {false, 28, "00010000000200000003000000040000", 16, true, 0, "313a303a323a303a333a303a343a3000", 16}, + {false, 28, "00010000000200000003000000040000", 15, false, 28, "", 0}, + {false, 28, "00010000000200000003000000040000", 1, false, 28, "", 0}, + {false, 28, "00000000000100000000000200000000", 64, true, 0, "3a3a313a303a303a323a303a30000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {false, 28, "00000000000100000000000200000000", 14, true, 0, "3a3a313a303a303a323a303a3000", 14}, + {false, 28, "00000000000100000000000200000000", 13, false, 28, "", 0}, + {false, 28, "00000000000100000000000200000000", 1, false, 28, "", 0}, + {false, 28, "00000000000000000000ffff01020304", 64, true, 0, "3a3a666666663a312e322e332e340000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {false, 28, "00000000000000000000ffff01020304", 15, true, 0, "3a3a666666663a312e322e332e3400", 15}, + {false, 28, "00000000000000000000ffff01020304", 14, false, 28, "", 0}, + {false, 28, "00000000000000000000ffff01020304", 1, false, 28, "", 0}, + {false, 28, "20010db8000000000000000000000017", 64, true, 0, "323030313a6462383a3a313700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {false, 28, "20010db8000000000000000000000017", 13, true, 0, "323030313a6462383a3a313700", 13}, + {false, 28, "20010db8000000000000000000000017", 12, false, 28, "", 0}, + {false, 28, "20010db8000000000000000000000017", 1, false, 28, "", 0}, + {false, 28, "00000000000000000000000000010000", 64, true, 0, "3a3a302e312e302e3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {false, 28, "00000000000000000000000000010000", 10, true, 0, "3a3a302e312e302e3000", 10}, + {false, 28, "00000000000000000000000000010000", 9, false, 28, "", 0}, + {false, 28, "00000000000000000000000000010000", 1, false, 28, "", 0}, + {false, 28, "00000000000000000000000000010000", 64, true, 0, "3a3a302e312e302e3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {false, 28, "00000000000000000000000000010000", 10, true, 0, "3a3a302e312e302e3000", 10}, + {false, 28, "00000000000000000000000000010000", 9, false, 28, "", 0}, + {false, 28, "00000000000000000000000000010000", 1, false, 28, "", 0}, + {false, 28, "ffffffffffffffffffffffffffffffff", 64, true, 0, "666666663a666666663a666666663a666666663a666666663a666666663a666666663a6666666600000000000000000000000000000000000000000000000000", 64}, + {false, 28, "ffffffffffffffffffffffffffffffff", 40, true, 0, "666666663a666666663a666666663a666666663a666666663a666666663a666666663a6666666600", 40}, + {false, 28, "ffffffffffffffffffffffffffffffff", 39, false, 28, "", 0}, + {false, 28, "ffffffffffffffffffffffffffffffff", 1, false, 28, "", 0}, + {false, 10, "01020304", 64, false, 47, "", 0}, + {false, 9999, "01020304", 64, false, 47, "", 0}, + {true, 2, "c0000211", 64, true, 0, "3139322e302e322e313700", 11}, + {true, 2, "c0000211", 11, true, 0, "3139322e302e322e313700", 11}, + {true, 2, "c0000211", 10, false, 28, "", 0}, + {true, 2, "c0000211", 1, false, 28, "", 0}, + {true, 2, "ff000764", 64, true, 0, "3235352e302e372e31303000", 12}, + {true, 2, "ff000764", 12, true, 0, "3235352e302e372e31303000", 12}, + {true, 2, "ff000764", 11, false, 28, "", 0}, + {true, 2, "ff000764", 1, false, 28, "", 0}, + {true, 28, "20010db8000000000000000000000017", 64, true, 0, "323030313a6462383a3a313700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {true, 28, "20010db8000000000000000000000017", 13, true, 0, "323030313a6462383a3a313700", 13}, + {true, 28, "20010db8000000000000000000000017", 12, false, 28, "", 0}, + {true, 28, "20010db8000000000000000000000017", 1, false, 28, "", 0}, + {true, 28, "00000000000000000000ffffc0000211", 64, true, 0, "3a3a666666663a3139322e302e322e31370000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {true, 28, "00000000000000000000ffffc0000211", 18, true, 0, "3a3a666666663a3139322e302e322e313700", 18}, + {true, 28, "00000000000000000000ffffc0000211", 17, false, 28, "", 0}, + {true, 28, "00000000000000000000ffffc0000211", 1, false, 28, "", 0}, + {true, 28, "00010002000300040005000600070000", 64, true, 0, "313a323a333a343a353a363a373a3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {true, 28, "00010002000300040005000600070000", 16, true, 0, "313a323a333a343a353a363a373a3000", 16}, + {true, 28, "00010002000300040005000600070000", 15, false, 28, "", 0}, + {true, 28, "00010002000300040005000600070000", 1, false, 28, "", 0}, + {true, 28, "abcdef01000000000000000000000001", 64, true, 0, "616263643a656630313a3a3100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {true, 28, "abcdef01000000000000000000000001", 13, true, 0, "616263643a656630313a3a3100", 13}, + {true, 28, "abcdef01000000000000000000000001", 12, false, 28, "", 0}, + {true, 28, "abcdef01000000000000000000000001", 1, false, 28, "", 0}, + {true, 28, "00000000000000000000ffff00000001", 64, true, 0, "3a3a666666663a302e302e302e310000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {true, 28, "00000000000000000000ffff00000001", 15, true, 0, "3a3a666666663a302e302e302e3100", 15}, + {true, 28, "00000000000000000000ffff00000001", 14, false, 28, "", 0}, + {true, 28, "00000000000000000000ffff00000001", 1, false, 28, "", 0}, + {true, 28, "0000000000000000ffff000000010000", 64, true, 0, "3a3a666666663a303a313a3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {true, 28, "0000000000000000ffff000000010000", 13, true, 0, "3a3a666666663a303a313a3000", 13}, + {true, 28, "0000000000000000ffff000000010000", 12, false, 28, "", 0}, + {true, 28, "0000000000000000ffff000000010000", 1, false, 28, "", 0}, + {true, 28, "00000000000000000000000000000001", 64, true, 0, "3a3a3100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {true, 28, "00000000000000000000000000000001", 4, true, 0, "3a3a3100", 4}, + {true, 28, "00000000000000000000000000000001", 3, false, 28, "", 0}, + {true, 28, "00000000000000000000000000000001", 1, false, 28, "", 0}, + {true, 28, "00000000000000000000000000000000", 64, true, 0, "3a3a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {true, 28, "00000000000000000000000000000000", 3, true, 0, "3a3a00", 3}, + {true, 28, "00000000000000000000000000000000", 2, false, 28, "", 0}, + {true, 28, "00000000000000000000000000000000", 1, false, 28, "", 0}, + {true, 28, "00010000000000000000000000000000", 64, true, 0, "313a3a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {true, 28, "00010000000000000000000000000000", 4, true, 0, "313a3a00", 4}, + {true, 28, "00010000000000000000000000000000", 3, false, 28, "", 0}, + {true, 28, "00010000000000000000000000000000", 1, false, 28, "", 0}, + {true, 28, "00000000000000000000000000000002", 64, true, 0, "3a3a3200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {true, 28, "00000000000000000000000000000002", 4, true, 0, "3a3a3200", 4}, + {true, 28, "00000000000000000000000000000002", 3, false, 28, "", 0}, + {true, 28, "00000000000000000000000000000002", 1, false, 28, "", 0}, + {true, 28, "00000000000000000000000001020304", 64, true, 0, "3a3a312e322e332e3400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {true, 28, "00000000000000000000000001020304", 10, true, 0, "3a3a312e322e332e3400", 10}, + {true, 28, "00000000000000000000000001020304", 9, false, 28, "", 0}, + {true, 28, "00000000000000000000000001020304", 1, false, 28, "", 0}, + {true, 28, "00010000000000020000000000000003", 64, true, 0, "313a303a303a323a3a33000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {true, 28, "00010000000000020000000000000003", 11, true, 0, "313a303a303a323a3a3300", 11}, + {true, 28, "00010000000000020000000000000003", 10, false, 28, "", 0}, + {true, 28, "00010000000000020000000000000003", 1, false, 28, "", 0}, + {true, 28, "00010000000200000003000000040000", 64, true, 0, "313a303a323a303a333a303a343a3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {true, 28, "00010000000200000003000000040000", 16, true, 0, "313a303a323a303a333a303a343a3000", 16}, + {true, 28, "00010000000200000003000000040000", 15, false, 28, "", 0}, + {true, 28, "00010000000200000003000000040000", 1, false, 28, "", 0}, + {true, 28, "00000000000100000000000200000000", 64, true, 0, "3a3a313a303a303a323a303a30000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {true, 28, "00000000000100000000000200000000", 14, true, 0, "3a3a313a303a303a323a303a3000", 14}, + {true, 28, "00000000000100000000000200000000", 13, false, 28, "", 0}, + {true, 28, "00000000000100000000000200000000", 1, false, 28, "", 0}, + {true, 28, "00000000000000000000ffff01020304", 64, true, 0, "3a3a666666663a312e322e332e340000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {true, 28, "00000000000000000000ffff01020304", 15, true, 0, "3a3a666666663a312e322e332e3400", 15}, + {true, 28, "00000000000000000000ffff01020304", 14, false, 28, "", 0}, + {true, 28, "00000000000000000000ffff01020304", 1, false, 28, "", 0}, + {true, 28, "20010db8000000000000000000000017", 64, true, 0, "323030313a6462383a3a313700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {true, 28, "20010db8000000000000000000000017", 13, true, 0, "323030313a6462383a3a313700", 13}, + {true, 28, "20010db8000000000000000000000017", 12, false, 28, "", 0}, + {true, 28, "20010db8000000000000000000000017", 1, false, 28, "", 0}, + {true, 28, "00000000000000000000000000010000", 64, true, 0, "3a3a302e312e302e3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {true, 28, "00000000000000000000000000010000", 10, true, 0, "3a3a302e312e302e3000", 10}, + {true, 28, "00000000000000000000000000010000", 9, false, 28, "", 0}, + {true, 28, "00000000000000000000000000010000", 1, false, 28, "", 0}, + {true, 28, "00000000000000000000000000010000", 64, true, 0, "3a3a302e312e302e3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 64}, + {true, 28, "00000000000000000000000000010000", 10, true, 0, "3a3a302e312e302e3000", 10}, + {true, 28, "00000000000000000000000000010000", 9, false, 28, "", 0}, + {true, 28, "00000000000000000000000000010000", 1, false, 28, "", 0}, + {true, 28, "ffffffffffffffffffffffffffffffff", 64, true, 0, "666666663a666666663a666666663a666666663a666666663a666666663a666666663a6666666600000000000000000000000000000000000000000000000000", 64}, + {true, 28, "ffffffffffffffffffffffffffffffff", 40, true, 0, "666666663a666666663a666666663a666666663a666666663a666666663a666666663a6666666600", 40}, + {true, 28, "ffffffffffffffffffffffffffffffff", 39, false, 28, "", 0}, + {true, 28, "ffffffffffffffffffffffffffffffff", 1, false, 28, "", 0}, + {true, 10, "01020304", 64, false, 47, "", 0}, + {true, 9999, "01020304", 64, false, 47, "", 0}, + {false, 28, "00000000000000000000000000000001", 65, true, 0, "3a3a3100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 65}, + {false, 2, "01020304", 65, true, 0, "312e322e332e3400", 8}, + {false, 28, "00000000000000000000000000000001", 128, true, 0, "3a3a3100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 128}, + {false, 2, "01020304", 128, true, 0, "312e322e332e3400", 8}, + {false, 28, "00000000000000000000000000000001", 1024, true, 0, "3a3a3100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 1024}, + {false, 2, "01020304", 1024, true, 0, "312e322e332e3400", 8}, + {false, 28, "00000000000000000000000000000001", 4096, true, 0, "3a3a3100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 4096}, + {false, 2, "01020304", 4096, true, 0, "312e322e332e3400", 8}, + {false, 28, "00000000000000000000000000000001", 65536, true, 0, "3a3a3100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 65536}, + {false, 2, "01020304", 65536, true, 0, "312e322e332e3400", 8}, + {true, 28, "00000000000000000000000000000001", 65, true, 0, "3a3a3100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 65}, + {true, 2, "01020304", 65, true, 0, "312e322e332e3400", 8}, + {true, 28, "00000000000000000000000000000001", 128, true, 0, "3a3a3100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 128}, + {true, 2, "01020304", 128, true, 0, "312e322e332e3400", 8}, + {true, 28, "00000000000000000000000000000001", 1024, true, 0, "3a3a3100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 1024}, + {true, 2, "01020304", 1024, true, 0, "312e322e332e3400", 8}, + {true, 28, "00000000000000000000000000000001", 4096, true, 0, "3a3a3100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 4096}, + {true, 2, "01020304", 4096, true, 0, "312e322e332e3400", 8}, + {true, 28, "00000000000000000000000000000001", 65536, true, 0, "3a3a3100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 65536}, + {true, 2, "01020304", 65536, true, 0, "312e322e332e3400", 8}, +}; +static const PadCase ConsolePaddingBoth[] = { + {28, "00000000000000000000000000000001", 4, 4}, + {28, "00000000000000000000000000000001", 5, 5}, + {28, "00000000000000000000000000000001", 16, 16}, + {28, "00000000000000000000000000000001", 45, 45}, + {28, "00000000000000000000000000000001", 46, 46}, + {28, "00000000000000000000000000000001", 47, 47}, + {28, "00000000000000000000000000000001", 64, 64}, + {28, "ffffffffffffffffffffffffffffffff", 64, 64}, + {2, "01020304", 8, 8}, + {2, "01020304", 16, 8}, + {2, "01020304", 64, 8}, +}; +static const PadCase ConsolePaddingLarge[] = { + {28, "00000000000000000000000000000001", 65, 65}, + {28, "00000000000000000000000000000001", 128, 128}, + {28, "00000000000000000000000000000001", 1024, 1024}, + {28, "00000000000000000000000000000001", 4096, 4096}, + {28, "00000000000000000000000000000001", 65536, 65536}, + {2, "01020304", 65, 8}, + {2, "01020304", 128, 8}, + {2, "01020304", 1024, 8}, + {2, "01020304", 4096, 8}, + {2, "01020304", 65536, 8}, +}; +static const PadCase ConsolePaddingUnderscored[] = { + {28, "00000000000000000000000000000001", 64, 64}, + {28, "00000000000000000000000000000001", 65, 65}, + {28, "00000000000000000000000000000001", 128, 128}, + {28, "00000000000000000000000000000001", 256, 256}, + {28, "00000000000000000000000000000001", 1024, 1024}, + {2, "01020304", 64, 8}, + {2, "01020304", 65, 8}, + {2, "01020304", 128, 8}, + {2, "01020304", 256, 8}, + {2, "01020304", 1024, 8}, +}; + +static unsigned HexValue(char digit) { return digit <= '9' ? static_cast(digit - '0') : static_cast(digit - 'a' + 10); } + +static std::size_t FromHex(const char* hex, unsigned char* out) { + std::size_t index = 0; + for (; hex[2 * index]; ++index) out[index] = static_cast(HexValue(hex[2 * index]) << 4 | HexValue(hex[2 * index + 1])); + return index; +} + +static bool SameBytes(const void* data, const char* hex) { + const auto* bytes = static_cast(data); + for (std::size_t index = 0; hex[2 * index]; ++index) + if (bytes[index] != (HexValue(hex[2 * index]) << 4 | HexValue(hex[2 * index + 1]))) return false; + return true; +} + +template +static bool Throws(TCall call) { + try { + call(); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + +static char Wide[70000]; + +static void CheckConsolePton() { + for (const PtonCase& sample : ConsolePton) { + unsigned char destination[16]; + std::memset(destination, 0xAA, sizeof(destination)); + *__error_nid_postfix() = 0; + const int result = sample.plain ? inet_pton_nid_postfix(sample.family, sample.text, destination) + : __inet_pton_nid_postfix(sample.family, sample.text, destination); + Require(result == sample.result); + Require(*__error_nid_postfix() == sample.error); + Require(SameBytes(destination, sample.bytes)); + } +} + +static void CheckConsoleNtop() { + for (const NtopCase& sample : ConsoleNtop) { + unsigned char source[16]{}; + FromHex(sample.source, source); + std::memset(Wide, 'Z', sizeof(Wide)); + *__error_nid_postfix() = 0; + const char* result = sample.plain ? inet_ntop_nid_postfix(sample.family, source, Wide, sample.size) + : __inet_ntop_nid_postfix(sample.family, source, Wide, sample.size); + Require(result == (sample.succeeds ? Wide : nullptr)); + Require(*__error_nid_postfix() == sample.error); + Require(SameBytes(Wide, sample.bytes)); + if (!sample.lastTouched && sample.succeeds) continue; + const std::size_t shown = std::strlen(sample.bytes) / 2; + for (std::size_t index = shown; index < sample.lastTouched; ++index) Require(Wide[index] != 'Z'); + for (std::size_t index = sample.lastTouched; index < sizeof(Wide); ++index) Require(Wide[index] == 'Z'); + } +} + +static void CheckConsolePadding(const PadCase* samples, std::size_t count, Ntop ntop) { + for (std::size_t sample = 0; sample < count; ++sample) { + unsigned char source[16]{}; + FromHex(samples[sample].source, source); + std::memset(Wide, 'Z', sizeof(Wide)); + Require(ntop(samples[sample].family, source, Wide, samples[sample].size) == Wide); + for (std::size_t index = std::strlen(Wide); index < samples[sample].lastTouched; ++index) Require(Wide[index] == '\0'); + for (std::size_t index = samples[sample].lastTouched; index < sizeof(Wide); ++index) Require(Wide[index] == 'Z'); + } +} + +static void CheckNullPointers() { + const unsigned char loopback[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}; + const unsigned char quad[16] = {1, 2, 3, 4}; + unsigned char bytes[16]{}; + char text[64]; + Require(Throws([&] { __inet_ntop_nid_postfix(28, nullptr, text, sizeof(text)); })); + Require(Throws([&] { __inet_ntop_nid_postfix(2, nullptr, text, sizeof(text)); })); + Require(Throws([&] { __inet_ntop_nid_postfix(28, loopback, nullptr, 46); })); + Require(Throws([&] { __inet_ntop_nid_postfix(2, quad, nullptr, 16); })); + Require(Throws([&] { inet_ntop_nid_postfix(28, nullptr, text, sizeof(text)); })); + Require(Throws([&] { inet_ntop_nid_postfix(28, loopback, nullptr, 46); })); + Require(Throws([&] { __inet_ntop_nid_postfix(2, nullptr, text, 0); })); + Require(Throws([&] { __inet_ntop_nid_postfix(28, nullptr, text, 1); })); + Require(Throws([&] { inet_ntop_nid_postfix(2, nullptr, text, 0); })); + Require(Throws([&] { inet_ntop_nid_postfix(28, nullptr, text, 1); })); + for (Ntop ntop : {__inet_ntop_nid_postfix, inet_ntop_nid_postfix}) { + *__error_nid_postfix() = 0; + Require(ntop(28, loopback, nullptr, 3) == nullptr && *__error_nid_postfix() == 28); + *__error_nid_postfix() = 0; + Require(ntop(2, quad, nullptr, 7) == nullptr && *__error_nid_postfix() == 28); + *__error_nid_postfix() = 0; + Require(ntop(2, quad, nullptr, 0) == nullptr && *__error_nid_postfix() == 28); + } + Require(Throws([&] { __inet_pton_nid_postfix(2, nullptr, bytes); })); + Require(Throws([&] { __inet_pton_nid_postfix(28, nullptr, bytes); })); + Require(Throws([&] { __inet_pton_nid_postfix(2, "1.2.3.4", nullptr); })); + Require(Throws([&] { __inet_pton_nid_postfix(28, "::1", nullptr); })); + Require(Throws([&] { inet_pton_nid_postfix(2, nullptr, bytes); })); + Require(Throws([&] { inet_pton_nid_postfix(2, "1.2.3.4", nullptr); })); + *__error_nid_postfix() = 0; + Require(__inet_pton_nid_postfix(2, "1.2.3", nullptr) == 0 && *__error_nid_postfix() == 0); + Require(__inet_pton_nid_postfix(28, "zzz", nullptr) == 0 && *__error_nid_postfix() == 0); + Require(__inet_pton_nid_postfix(2, "", nullptr) == 0 && *__error_nid_postfix() == 0); + Require(inet_pton_nid_postfix(2, "1.2.3", nullptr) == 0 && *__error_nid_postfix() == 0); + Require(inet_pton_nid_postfix(28, "zzz", nullptr) == 0 && *__error_nid_postfix() == 0); + Require(inet_pton_nid_postfix(2, "", nullptr) == 0 && *__error_nid_postfix() == 0); + *__error_nid_postfix() = 0; + Require(__inet_ntop_nid_postfix(9999, nullptr, text, sizeof(text)) == nullptr && *__error_nid_postfix() == 47); + *__error_nid_postfix() = 0; + Require(__inet_ntop_nid_postfix(9999, bytes, nullptr, 46) == nullptr && *__error_nid_postfix() == 47); + *__error_nid_postfix() = 0; + Require(__inet_pton_nid_postfix(9999, nullptr, bytes) == -1 && *__error_nid_postfix() == 47); + *__error_nid_postfix() = 0; + Require(__inet_pton_nid_postfix(9999, "1.2.3.4", nullptr) == -1 && *__error_nid_postfix() == 47); + *__error_nid_postfix() = 0; + Require(inet_ntop_nid_postfix(9999, nullptr, text, sizeof(text)) == nullptr && *__error_nid_postfix() == 47); + *__error_nid_postfix() = 0; + Require(inet_ntop_nid_postfix(9999, quad, nullptr, 46) == nullptr && *__error_nid_postfix() == 47); + *__error_nid_postfix() = 0; + Require(inet_pton_nid_postfix(9999, nullptr, bytes) == -1 && *__error_nid_postfix() == 47); + *__error_nid_postfix() = 0; + Require(inet_pton_nid_postfix(9999, "1.2.3.4", nullptr) == -1 && *__error_nid_postfix() == 47); +} + +static void CheckErrnoKept() { + unsigned char bytes[16]{}; + char text[64]; + *__error_nid_postfix() = 13; + Require(__inet_pton_nid_postfix(2, "1.2.3.4", bytes) == 1 && *__error_nid_postfix() == 13); + Require(__inet_pton_nid_postfix(2, "1.2.3", bytes) == 0 && *__error_nid_postfix() == 13); + Require(__inet_pton_nid_postfix(28, "::1", bytes) == 1 && *__error_nid_postfix() == 13); + Require(__inet_ntop_nid_postfix(2, bytes, text, sizeof(text)) == text && *__error_nid_postfix() == 13); +} + int main() { + Require(htonl_nid_postfix(0x11223344u) == 0x44332211u && ntohl_nid_postfix(0x44332211u) == 0x11223344u); + Require(htons_nid_postfix(0x1234) == 0x3412 && ntohs_nid_postfix(0x3412) == 0x1234); + const std::uint32_t loopback = htonl_nid_postfix(0x7f000001u); + unsigned char loopbackBytes[4]; + std::memcpy(loopbackBytes, &loopback, sizeof(loopback)); + Require(loopbackBytes[0] == 127 && loopbackBytes[3] == 1); for (int family : {2, 28}) { const char* input = family == 2 ? "192.0.2.17" : "2001:db8::17"; std::array address{}; @@ -76,4 +641,14 @@ int main() { Require(__inet_ntop_nid_postfix(2, fourth, widest, 12) == widest && std::strcmp(widest, "255.0.7.100") == 0); Require(__inet_ntop_nid_postfix(2, fourth, widest, 11) == nullptr && *__error_nid_postfix() == 28); Require(__inet_ntop_nid_postfix(10, fourth, widest, sizeof(widest)) == nullptr && *__error_nid_postfix() == 47); + + CheckConsolePton(); + CheckConsoleNtop(); + CheckConsolePadding(ConsolePaddingBoth, sizeof(ConsolePaddingBoth) / sizeof(ConsolePaddingBoth[0]), __inet_ntop_nid_postfix); + CheckConsolePadding(ConsolePaddingBoth, sizeof(ConsolePaddingBoth) / sizeof(ConsolePaddingBoth[0]), inet_ntop_nid_postfix); + CheckConsolePadding(ConsolePaddingUnderscored, sizeof(ConsolePaddingUnderscored) / sizeof(ConsolePaddingUnderscored[0]), __inet_ntop_nid_postfix); + CheckConsolePadding(ConsolePaddingLarge, sizeof(ConsolePaddingLarge) / sizeof(ConsolePaddingLarge[0]), __inet_ntop_nid_postfix); + CheckConsolePadding(ConsolePaddingLarge, sizeof(ConsolePaddingLarge) / sizeof(ConsolePaddingLarge[0]), inet_ntop_nid_postfix); + CheckNullPointers(); + CheckErrnoKept(); } diff --git a/core/libs/tests/GuestAgcDriverDebugUnavailable.cpp b/core/libs/tests/GuestAgcDriverDebugUnavailable.cpp index 89ca36d6f2..72b97bd56f 100644 --- a/core/libs/tests/GuestAgcDriverDebugUnavailable.cpp +++ b/core/libs/tests/GuestAgcDriverDebugUnavailable.cpp @@ -10,9 +10,31 @@ int APS5_VABI sceAgcDriverSetSubmitValidationMode(std::uint32_t); int APS5_VABI sceAgcDriverGetSubmitValidationMode(std::uint32_t*); int APS5_VABI sceAgcDriverSetSubmitValidationConfig(const void*); int APS5_VABI sceAgcDriverGetSubmitValidationConfig(void*); +int APS5_VABI sceAgcDriverSetValidationDisabled(); +int APS5_VABI sceAgcDriverGetCaptureConfig(); +int APS5_VABI sceAgcDriverSetCaptureConfig(); +int APS5_VABI sceAgcDriverGetGpuPrintfWorkArea(); +int APS5_VABI sceAgcDriverSetGpuPrintfEnabled(); +int APS5_VABI sceAgcDriverDbgReadRegister(); +int APS5_VABI sceAgcDriverDbgWriteRegister(); +int APS5_VABI sceAgcDriverGetGpuRefClks(); +int APS5_VABI sceAgcDriverIsSuspendPointInFlight(); +int APS5_VABI sceAgcDriverIsPaDebug(); +int APS5_VABI sceAgcDriverGetExceptionStatus(); +int APS5_VABI sceAgcDriverGetTraceInitiator(); +int APS5_VABI sceAgcDriverRegisterGpuResetCallbacks(); +int APS5_VABI sceAgcDriverUnregisterGpuResetCallbacks(); +int APS5_VABI sceAgdResetGpu(); +int APS5_VABI sceAgcDriverSysSubmitFlipHandleProxy(); +int APS5_VABI sceAgcDriverUserDataImmediateWrite(); +int APS5_VABI sceAgcDriverUserDataImmediateWritePacket(); +int APS5_VABI sceAgcDriverGetCoredumpMode(std::uint32_t*); +int APS5_VABI sceAgcDriverAllocateToolMemoryForGpuReset(std::uint64_t, std::uint64_t, std::uint64_t, void*); +int APS5_VABI sceAgcDriverGetAllocatedToolMemoryForGpuReset(std::uint64_t, void*); } static constexpr int DebugUnavailable = static_cast(0x8A6C1000); +static constexpr int FeatureUnavailable = static_cast(0x8A6D0001); static void Require(bool value) { if (!value) std::abort(); } int main() { @@ -35,4 +57,29 @@ int main() { Require(sceAgcDriverGetSubmitValidationConfig(config.data()) == DebugUnavailable); Require(sceAgcDriverGetSubmitValidationConfig(nullptr) == DebugUnavailable); for (const std::uint8_t byte : config) Require(byte == 0x5A); + + Require(sceAgcDriverSetValidationDisabled() == DebugUnavailable); + Require(sceAgcDriverGetCaptureConfig() == DebugUnavailable && sceAgcDriverSetCaptureConfig() == DebugUnavailable); + Require(sceAgcDriverGetGpuPrintfWorkArea() == FeatureUnavailable && sceAgcDriverSetGpuPrintfEnabled() == FeatureUnavailable); + Require(sceAgcDriverDbgReadRegister() == 0 && sceAgcDriverDbgWriteRegister() == FeatureUnavailable); + Require(sceAgcDriverGetGpuRefClks() == FeatureUnavailable && sceAgcDriverIsSuspendPointInFlight() == FeatureUnavailable); + Require(sceAgcDriverIsPaDebug() == 0 && sceAgcDriverGetExceptionStatus() == 0 && sceAgcDriverGetTraceInitiator() == 0); + Require(sceAgcDriverRegisterGpuResetCallbacks() == 0 && sceAgcDriverUnregisterGpuResetCallbacks() == 0); + Require(sceAgdResetGpu() == static_cast(0x8A6C0045)); + Require(sceAgcDriverSysSubmitFlipHandleProxy() == 0); + Require(sceAgcDriverUserDataImmediateWrite() == 0 && sceAgcDriverUserDataImmediateWritePacket() == 0); + + std::uint32_t coredumpMode = 0xA5A5A5A5u; + Require(sceAgcDriverGetCoredumpMode(nullptr) == static_cast(0x8A6D0003)); + Require(sceAgcDriverGetCoredumpMode(&coredumpMode) == 0 && coredumpMode == 0); + + std::array range{}; + range.fill(0x5A); + Require(sceAgcDriverAllocateToolMemoryForGpuReset(1, 2, 3, range.data()) == FeatureUnavailable); + for (std::size_t i = 0; i < range.size(); ++i) Require(range[i] == (i < 12 ? 0 : 0x5A)); + Require(sceAgcDriverAllocateToolMemoryForGpuReset(1, 2, 3, nullptr) == FeatureUnavailable); + range.fill(0x5A); + Require(sceAgcDriverGetAllocatedToolMemoryForGpuReset(1, range.data()) == FeatureUnavailable); + for (std::size_t i = 0; i < range.size(); ++i) Require(range[i] == (i < 12 ? 0 : 0x5A)); + Require(sceAgcDriverGetAllocatedToolMemoryForGpuReset(1, nullptr) == FeatureUnavailable); } diff --git a/core/libs/tests/GuestAgcDriverResources.cpp b/core/libs/tests/GuestAgcDriverResources.cpp index 0406b512c3..fc55b15a8a 100644 --- a/core/libs/tests/GuestAgcDriverResources.cpp +++ b/core/libs/tests/GuestAgcDriverResources.cpp @@ -28,6 +28,9 @@ int APS5_VABI sceAgcDriverFindResourcesPublic(const void*, void*); bool APS5_VABI sceAgcDriverIsCaptureInProgress(void); bool APS5_VABI sceAgcDriverIsTraceInProgress(void); bool APS5_VABI sceAgcDriverIsSubmitValidationEnabled(void); +int APS5_VABI sceAgcDriverRegisterOwnerForSystem(); +int APS5_VABI sceAgcDriverInitResourceRegistrationInterface(); +int APS5_VABI sceAgcDriverSetResourceRegistrationInfo(); } static constexpr int Unavailable = static_cast(0x8A6C9018); @@ -94,4 +97,7 @@ int main() { Require(!sceAgcDriverIsCaptureInProgress()); Require(!sceAgcDriverIsTraceInProgress()); Require(!sceAgcDriverIsSubmitValidationEnabled()); + Require(sceAgcDriverRegisterOwnerForSystem() == Unavailable); + Require(sceAgcDriverInitResourceRegistrationInterface() == Unavailable); + Require(sceAgcDriverSetResourceRegistrationInfo() == 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/GuestAjmVibration.cpp b/core/libs/tests/GuestAjmVibration.cpp new file mode 100644 index 0000000000..11351dcb6f --- /dev/null +++ b/core/libs/tests/GuestAjmVibration.cpp @@ -0,0 +1,171 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceAjmInitialize(std::int64_t, std::uint32_t*); +int APS5_VABI sceAjmFinalize(std::uint32_t); +int APS5_VABI sceAjmDecAt9ParseConfigData(const void*, AjmDecAt9ConfigDataInfo*); +int APS5_VABI sceAjmInstanceCreate(std::uint32_t, std::uint32_t, std::uint64_t, std::uint32_t*); +int APS5_VABI sceAjmInstanceDestroy(std::uint32_t, std::uint32_t); +int APS5_VABI sceAjmBatchInitialize(void*, std::size_t, AjmBatchInfo*); +int APS5_VABI sceAjmBatchJobInitialize(AjmBatchInfo*, std::uint32_t, const void*, std::size_t, void*); +int APS5_VABI sceAjmBatchJobRun(AjmBatchInfo*, std::uint32_t, std::uint64_t, const void*, std::size_t, void*, std::size_t, void*, std::size_t); +int APS5_VABI sceAjmBatchStart(std::uint32_t, const AjmBatchInfo*, int, AjmBatchError*, std::uint32_t*); +int APS5_VABI sceAjmBatchWait(std::uint32_t, std::uint32_t, std::uint32_t, AjmBatchError*); +} + +namespace { +void Require(bool value, const char* message) { + if (!value) throw std::runtime_error(message); +} + +void Submit(std::uint32_t context, const AjmBatchInfo& info) { + AjmBatchError error{}; + std::uint32_t id = 0; + Require(sceAjmBatchStart(context, &info, 0, &error, &id) == 0 && sceAjmBatchWait(context, id, 0, &error) == 0, "batch submit"); +} + +std::array Config(unsigned channels) { + return {0xFE, static_cast(channels == 1 ? 0x7C : 0x7E), 0x03, 0xF0}; +} + +std::vector Packet(const std::vector& amplitudes) { + std::vector bytes; + std::size_t position = 0; + const auto put = [&](unsigned value, unsigned bits) { + for (unsigned bit = bits; bit > 0; --bit) { + if (position / 8 == bytes.size()) bytes.push_back(0); + bytes[position / 8] |= ((value >> (bit - 1)) & 1u) << (7 - position % 8); + ++position; + } + }; + for (unsigned frame = 0; frame < 4; ++frame) { + for (const auto amplitude : amplitudes) { + put(frame != 0, 1); + put(0, 1); + put(0, 1); + put(0, 4); + put(25, 5); + put(6, 3); + put(static_cast(amplitude) & 255u, 8); + put(static_cast(-amplitude) & 255u, 8); + while (position % 8 != 0) put(0, 1); + } + } + bytes.resize(128, 1); + return bytes; +} + +struct Stream { + std::int32_t result = -1; + std::int32_t internalResult = -1; + std::int32_t consumed = -1; + std::int32_t produced = -1; + std::uint64_t samples = 0; + std::uint32_t frames = 0; + std::uint32_t reserved = 0; +}; + +template +std::vector Decode(std::uint32_t context, unsigned channels, unsigned encoding, const std::vector& amplitudes, unsigned profile = 7, bool partial = false, bool malformed = false) { + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, 1, static_cast(encoding) << 7u, &instance) == 0, "create vibration instance"); + auto config = Config(channels); + config[1] = static_cast((profile << 4) | (config[1] & 15u)); + std::vector batch(4096); + AjmBatchInfo info{}; + std::int32_t initialized[2] = {-1, -1}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0, "initialize batch"); + Require(sceAjmBatchJobInitialize(&info, instance, config.data(), config.size(), initialized) == 0, "append initialize"); + Submit(context, info); + Require(initialized[0] == 0 && initialized[1] == 0, "initialize vibration decoder"); + auto packet = Packet(amplitudes); + const auto samples = profile == 1 ? 256 : profile == 4 ? 512 : 1024; + std::vector pcm(samples * channels); + constexpr std::uint64_t flags = (1ull << 47) | (1ull << 12); + const auto run = [&](const std::vector& input, std::size_t size) { + Stream stream; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0, "reset batch"); + Require(sceAjmBatchJobRun(&info, instance, flags, input.data(), size, pcm.data(), pcm.size() * sizeof(T), &stream, sizeof(stream)) == 0, "append run"); + Submit(context, info); + return stream; + }; + if (partial) { + std::fill(pcm.begin(), pcm.end(), T{123}); + const auto stream = run(packet, packet.size() - 1); + Require(stream.result == 8 && stream.consumed == 0 && stream.produced == 0, "partial superframe accounting"); + Require(std::all_of(pcm.begin(), pcm.end(), [](T value) { return value == T{123}; }), "partial input changed PCM"); + } + if (malformed) { + auto broken = packet; + broken[0] |= 0x80; + const auto stream = run(broken, broken.size()); + Require((stream.result & 2) != 0 && stream.consumed == 0 && stream.produced == 0, "invalid first-frame flag"); + } + const auto stream = run(packet, packet.size()); + Require(stream.result == 0 && stream.internalResult == 0, "decode vibration superframe"); + Require(stream.consumed == 128 && stream.produced == static_cast(pcm.size() * sizeof(T)), "consumed/produced accounting"); + Require(stream.samples == static_cast(samples) && stream.frames == 4, "decoded sample/frame accounting"); + Require(sceAjmInstanceDestroy(context, instance) == 0, "destroy instance"); + return pcm; +} + +void Parse() { + for (const auto index : {6u, 7u}) { + for (const auto profile : {1u, 4u, 7u}) { + auto config = Config(index == 6 ? 1 : 2); + config[1] = static_cast((profile << 4) | (index << 1)); + AjmDecAt9ConfigDataInfo info{}; + Require(sceAjmDecAt9ParseConfigData(config.data(), &info) == 0, "parse vibration config"); + const unsigned samples = profile == 1 ? 64 : profile == 4 ? 128 : 256; + Require(info.channels == (index == 6 ? 1u : 2u) && info.sample_rate == (profile == 1 ? 12000u : profile == 4 ? 24000u : 48000u), "vibration layout/rate"); + Require(info.superframe_size == 128 && info.frame_samples_per_channel == samples && info.superframe_samples_per_channel == samples * 4, "vibration frame geometry"); + } + } + auto invalid = Config(2); + invalid[1] |= 1; + AjmDecAt9ConfigDataInfo info; + std::memset(&info, 0xAA, sizeof(info)); + Require(sceAjmDecAt9ParseConfigData(invalid.data(), &info) == static_cast(0x80930005), "invalid version accepted"); + Require(info.superframe_size == 0xAAAAAAAAu, "invalid parse changed output"); +} +} + +int main() { + try { + Parse(); + std::uint32_t context = 0; + Require(sceAjmInitialize(0, &context) == 0, "initialize AJM"); + const auto left = Decode(context, 1, 2, {1}); + const auto right = Decode(context, 1, 2, {2}); + const auto stereo = Decode(context, 2, 2, {1, 2}, 7, true, true); + Require(std::any_of(left.begin(), left.end(), [](float value) { return std::abs(value) > 0.000001f; }), "vibration decoded to silence"); + for (std::size_t sample = 0; sample < left.size(); ++sample) { + Require(std::isfinite(stereo[2 * sample]) && std::isfinite(stereo[2 * sample + 1]), "non-finite PCM"); + Require(stereo[2 * sample] == left[sample] && stereo[2 * sample + 1] == right[sample], "dual-mono interleaving or reset history"); + } + const auto pcm16 = Decode(context, 2, 0, {1, 2}); + const auto pcm32 = Decode(context, 2, 1, {1, 2}); + for (std::size_t sample = 0; sample < stereo.size(); ++sample) { + Require(std::abs(static_cast(pcm16[sample]) / 32768.0 - stereo[sample]) <= 1.0 / 32768.0, "PCM16 conversion"); + Require(std::abs(static_cast(pcm32[sample]) / 2147483648.0 - stereo[sample]) <= 1.0 / 2147483648.0, "PCM32 conversion"); + } + for (const auto profile : {1u, 4u}) { + const auto mono = Decode(context, 1, 2, {1}, profile); + const auto dual = Decode(context, 2, 2, {1, 1}, profile); + Require(std::any_of(mono.begin(), mono.end(), [](float value) { return std::abs(value) > 0.000001f; }), "lower-rate vibration decoded to silence"); + for (std::size_t sample = 0; sample < mono.size(); ++sample) Require(dual[2 * sample] == mono[sample] && dual[2 * sample + 1] == mono[sample], "lower-rate interleaving"); + } + Require(sceAjmFinalize(context) == 0, "finalize AJM"); + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/tests/GuestAmpr.cpp b/core/libs/tests/GuestAmpr.cpp index 2340688d38..ca17fc2c23 100644 --- a/core/libs/tests/GuestAmpr.cpp +++ b/core/libs/tests/GuestAmpr.cpp @@ -243,6 +243,16 @@ void TestRejectedArguments() { Require(sceAmprMeasureCommandSizePushMarkerWithColor(nullptr, color) == rejected); Require(sceAmprMeasureCommandSizeSetMarker(nullptr) == rejected); Require(sceAmprMeasureCommandSizeSetMarkerWithColor(nullptr, color) == rejected); + + std::uint64_t gatherState = 0; + std::uint64_t scatterState = 0; + std::array memory{}; + Require(sceAmprCommandBufferConstructor(nullptr) == invalidArgument); + Require(sceAmprAprCommandBufferConstructor(nullptr, &gatherState, &scatterState) == invalidArgument); + Require(sceAmprAprCommandBufferConstructor(&recorder.buffer, nullptr, &scatterState) == invalidArgument); + Require(sceAmprAprCommandBufferConstructor(&recorder.buffer, &gatherState, nullptr) == invalidArgument); + Require(sceAmprCommandBufferReset(nullptr) == invalidArgument); + Require(sceAmprCommandBufferSetBuffer(nullptr, memory.data(), memory.size()) == invalidArgument); } void TestFullBuffer() { diff --git a/core/libs/tests/GuestAmprThreads.cpp b/core/libs/tests/GuestAmprThreads.cpp index 264ff62a6d..403c445304 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 @@ -60,7 +61,7 @@ int APS5_VABI _Thrd_join_nid_postfix(Pthread thread, int* code); int APS5_VABI _Thrd_sleep_nid_postfix(const GuestXtime* target, GuestXtime* remaining); void APS5_VABI _Lock_shared_ptr_spin_lock_nid_postfix(void); void APS5_VABI _Unlock_shared_ptr_spin_lock_nid_postfix(void); -int APS5_VABI wcstombs_s_nid_postfix(std::size_t* result, char* destination, std::size_t capacity, const std::uint16_t* source, std::size_t limit); +int APS5_VABI sceAmprWcstombsS(std::size_t* result, char* destination, std::size_t capacity, const std::uint16_t* source, std::size_t limit); int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); int APS5_VABI pthread_detach_nid_postfix(Pthread thread); int APS5_VABI clock_gettime_nid_postfix(int clockId, KernelTimespec* tp); @@ -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); @@ -215,42 +233,42 @@ static void TestWcstombs() { char buffer[8]; std::memset(buffer, 'x', sizeof(buffer)); - Require(wcstombs_s_nid_postfix(&result, buffer, sizeof(buffer), text, sizeof(buffer)) == 0); + Require(sceAmprWcstombsS(&result, buffer, sizeof(buffer), text, sizeof(buffer)) == 0); Require(result == 3 && std::strcmp(buffer, "abc") == 0); std::memset(buffer, 'x', sizeof(buffer)); - Require(wcstombs_s_nid_postfix(&result, buffer, sizeof(buffer), text, 2) == 0); + Require(sceAmprWcstombsS(&result, buffer, sizeof(buffer), text, 2) == 0); Require(result == 2 && std::strcmp(buffer, "ab") == 0); result = 0; - Require(wcstombs_s_nid_postfix(&result, nullptr, 0, text, 0) == 0); + Require(sceAmprWcstombsS(&result, nullptr, 0, text, 0) == 0); Require(result == 3); std::memset(buffer, 'x', sizeof(buffer)); - Require(wcstombs_s_nid_postfix(&result, buffer, 3, text, 3) == GuestErange); + Require(sceAmprWcstombsS(&result, buffer, 3, text, 3) == GuestErange); Require(result == Failed && buffer[0] == '\0'); result = 0; - Require(wcstombs_s_nid_postfix(&result, nullptr, 4, text, 4) == GuestEinval); + Require(sceAmprWcstombsS(&result, nullptr, 4, text, 4) == GuestEinval); Require(result == Failed); std::memset(buffer, 'x', sizeof(buffer)); - Require(wcstombs_s_nid_postfix(&result, buffer, sizeof(buffer), nullptr, sizeof(buffer)) == GuestEinval); + Require(sceAmprWcstombsS(&result, buffer, sizeof(buffer), nullptr, sizeof(buffer)) == GuestEinval); Require(result == Failed && buffer[0] == '\0'); std::memset(buffer, 'x', sizeof(buffer)); - Require(wcstombs_s_nid_postfix(nullptr, buffer, sizeof(buffer), text, sizeof(buffer)) == GuestEinval); + Require(sceAmprWcstombsS(nullptr, buffer, sizeof(buffer), text, sizeof(buffer)) == GuestEinval); Require(buffer[0] == '\0'); std::memset(buffer, 'x', sizeof(buffer)); - Require(wcstombs_s_nid_postfix(&result, buffer, 0, text, 4) == GuestEinval); + Require(sceAmprWcstombsS(&result, buffer, 0, text, 4) == GuestEinval); Require(result == Failed && buffer[0] == 'x'); std::memset(buffer, 'x', sizeof(buffer)); - Require(wcstombs_s_nid_postfix(&result, buffer, sizeof(buffer), wide, sizeof(buffer)) == GuestEilseq); + Require(sceAmprWcstombsS(&result, buffer, sizeof(buffer), wide, sizeof(buffer)) == GuestEilseq); Require(result == Failed && buffer[0] == 'a' && buffer[1] == '\0'); - Require(wcstombs_s_nid_postfix(&result, nullptr, 0, wide, 0) == GuestEilseq); + Require(sceAmprWcstombsS(&result, nullptr, 0, wide, 0) == GuestEilseq); Require(result == Failed); } 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/GuestAppInfo.cpp b/core/libs/tests/GuestAppInfo.cpp index 5421462aea..860ff13718 100644 --- a/core/libs/tests/GuestAppInfo.cpp +++ b/core/libs/tests/GuestAppInfo.cpp @@ -12,6 +12,7 @@ extern "C" { int APS5_VABI sceKernelGetAppInfo(int pid, void* info); int APS5_VABI getpid_nid_postfix(void); int APS5_VABI sceKernelUuidCreate(std::uint32_t* uuid); +int APS5_VABI sceKernelCreateUuidV4(std::uint8_t* uuid); } static constexpr int SCE_KERNEL_ERROR_EPERM = static_cast(0x80020001); @@ -109,4 +110,11 @@ int main(int argc, char** argv) { Require(sceKernelUuidCreate(first) == 0 && sceKernelUuidCreate(second) == 0); for (const std::uint32_t* uuid : {first, second}) Require((uuid[1] >> 28) == 4u && (uuid[2] & 0xc0u) == 0x80u); Require(std::memcmp(first, second, sizeof(first)) != 0); + + Require(sceKernelCreateUuidV4(nullptr) == SCE_KERNEL_ERROR_EINVAL); + std::uint8_t third[16]{}; + std::uint8_t fourth[16]{}; + Require(sceKernelCreateUuidV4(third) == 0 && sceKernelCreateUuidV4(fourth) == 0); + for (const std::uint8_t* uuid : {third, fourth}) Require((uuid[6] >> 4) == 4u && (uuid[8] & 0xc0u) == 0x80u); + Require(std::memcmp(third, fourth, sizeof(third)) != 0); } diff --git a/core/libs/tests/GuestAprRead.cpp b/core/libs/tests/GuestAprRead.cpp index d2f12108a7..a8ba636dc3 100644 --- a/core/libs/tests/GuestAprRead.cpp +++ b/core/libs/tests/GuestAprRead.cpp @@ -228,13 +228,39 @@ private: void* pointer = nullptr; void* handler = nullptr; }; + +struct SmallStackRead { + const Fixture* fixture = nullptr; + std::vector* storage = nullptr; + std::string error; +}; + +DWORD WINAPI ReadOnSmallStack(void* parameter) { + auto& read = *static_cast(parameter); + try { + CheckRead(*read.fixture, read.storage->data() + 17, 65537, 0); + } catch (const std::exception& failure) { + read.error = failure.what(); + } + return 0; +} + +void TestSmallStack(const Fixture& fixture) { + std::vector storage(131200, 0xCC); + SmallStackRead read{&fixture, &storage, {}}; + const HANDLE thread = CreateThread(nullptr, 64u * 1024u, ReadOnSmallStack, &read, STACK_SIZE_PARAM_IS_A_RESERVATION, nullptr); + Require(thread != nullptr, "cannot start the small-stack APR reader"); + WaitForSingleObject(thread, INFINITE); + CloseHandle(thread); + Require(read.error.empty(), read.error.c_str()); +} #endif } int main(int argc, char** argv) { try { - Require(argc == 2, "expected host, direct, eof, errors or fault"); + Require(argc == 2, "expected host, direct, eof, errors, streams, fault or stack"); const Fixture fixture; const std::string mode = argv[1]; if (mode == "host") { @@ -254,6 +280,8 @@ int main(int argc, char** argv) { const FaultBuffer storage; TestRead(fixture, storage.Data()); Require(committedPages > 2, "APR did not exercise demand-committed destination pages"); + } else if (mode == "stack") { + TestSmallStack(fixture); #endif } else { throw std::runtime_error("unknown APR read case"); diff --git a/core/libs/tests/GuestArenaHost.cpp b/core/libs/tests/GuestArenaHost.cpp index 6a9a907466..e7863563a6 100644 --- a/core/libs/tests/GuestArenaHost.cpp +++ b/core/libs/tests/GuestArenaHost.cpp @@ -1,5 +1,6 @@ #include "prx/libc/include/GuestArena.hpp" +#include #include #include #include @@ -20,7 +21,11 @@ using namespace GuestArena; namespace { constexpr std::uintptr_t ArenaStart = 0x0000000200000000ull; +constexpr std::uintptr_t SystemReservedStart = 0x00000007FFFFC000ull; +constexpr std::uintptr_t SystemReservedEnd = 0x0000001000000000ull; constexpr std::uintptr_t ApplicationAreaEnd = 0x000000FC00000000ull; +constexpr std::uintptr_t ReservationChunk = 1ull << 30; +constexpr std::size_t GapBytes = 0x10000; constexpr std::uint32_t ProtectionNoAccess = 0x01; constexpr std::uint32_t ProtectionReadOnly = 0x02; constexpr std::uint32_t ProtectionReadWrite = 0x04; @@ -51,6 +56,15 @@ std::vector RegionsInArenaRange() { return regions; } +const std::vector RegionsBeforeArena = RegionsInArenaRange(); + +bool Overlaps(const std::vector& regions, std::uintptr_t start, std::uintptr_t end) { + for (const auto& region : regions) { + if (region.start < end && start < region.end) return true; + } + return false; +} + void CheckAvailable() { Require(GuestArenaAvailable_nid_postfix(), "the guest arena is unavailable on macOS"); std::uintptr_t base = 0; @@ -88,6 +102,24 @@ void CheckHostRegionsKept() { std::printf("%zu host regions inside the arena range are protected\n", hostRegions); } +void CheckGapBesideHostRegion() { + Require(GuestArenaAvailable_nid_postfix(), "the guest arena is unavailable on macOS"); + Require(RegionsBeforeArena.size() < RegionsInArenaRange().size(), "the host map was read after the guest arena reserved its range"); + for (const auto& host : RegionsBeforeArena) { + const auto chunk = host.start & ~(ReservationChunk - 1); + for (auto gap = std::max(chunk, ArenaStart); gap + GapBytes <= chunk + ReservationChunk && gap + GapBytes <= ApplicationAreaEnd; gap += GapBytes) { + if (Overlaps(RegionsBeforeArena, gap, gap + GapBytes) || (gap < SystemReservedEnd && SystemReservedStart < gap + GapBytes)) continue; + auto* pointer = reinterpret_cast(gap); + GuestArenaMarkUsed_nid_postfix(pointer, GapBytes); + GuestArenaRelease_nid_postfix(pointer, GapBytes); + std::printf("free range 0x%llx beside host region 0x%llx-0x%llx accepted\n", static_cast(gap), + static_cast(host.start), static_cast(host.end)); + return; + } + } + std::puts("no free range shares a 1 GiB reservation chunk with a host region on this host; nothing to check"); +} + void CheckCommitAndProtect() { constexpr std::size_t bytes = 0x10000; auto* pointer = static_cast(GuestArenaAllocate_nid_postfix(bytes, bytes)); @@ -106,11 +138,12 @@ void CheckCommitAndProtect() { int main(int argc, char** argv) { try { - Require(argc == 2, "expected available, unmappable, host or commit"); + Require(argc == 2, "expected available, unmappable, host, gap or commit"); const std::string_view mode(argv[1]); if (mode == "available") CheckAvailable(); else if (mode == "unmappable") CheckSkipsUnmappable(); else if (mode == "host") CheckHostRegionsKept(); + else if (mode == "gap") CheckGapBesideHostRegion(); else if (mode == "commit") CheckCommitAndProtect(); else throw std::runtime_error("unknown guest arena test"); std::printf("guest arena %s passed\n", argv[1]); diff --git a/core/libs/tests/GuestAt9Enc.cpp b/core/libs/tests/GuestAt9Enc.cpp new file mode 100644 index 0000000000..2da60d41b3 --- /dev/null +++ b/core/libs/tests/GuestAt9Enc.cpp @@ -0,0 +1,42 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +extern "C" { +int32_t APS5_VABI sceAt9EncClearContext(void* pAt9EncWorkArea, int32_t* pInternalResult); +int32_t APS5_VABI sceAt9EncCreateEncoder(void* pAt9EncWorkArea, void* pAt9EncParam, int32_t memSize, int32_t* pInternalResult); +int32_t APS5_VABI sceAt9EncEncode(void* pAt9EncWorkArea, const void* pInputBuf, void* pOutputBuf, uint32_t inputBufSize, uint32_t outputBufSize, uint32_t* pOutputDataByte, uint32_t* pUsedBufSize, int32_t* pInternalResult); +int32_t APS5_VABI sceAt9EncFlush(void* pAt9EncWorkArea, void* pOutputBuf, uint32_t outputBufSize, uint32_t* pOutputDataByte, int32_t* pInternalResult); +int32_t APS5_VABI sceAt9EncQueryMemSize(void* pAt9EncParam, int32_t* pMemSize, int32_t* pInternalResult); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +template +bool Throws(Call call) { + try { + static_cast(call()); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + +} + +int main() { + int32_t memSize = 0; + int32_t internalResult = 0; + + Require(Throws([&] { return sceAt9EncClearContext(nullptr, &internalResult); })); + Require(Throws([&] { return sceAt9EncCreateEncoder(nullptr, nullptr, 0, &internalResult); })); + Require(Throws([&] { return sceAt9EncEncode(nullptr, nullptr, nullptr, 0, 0, nullptr, nullptr, &internalResult); })); + Require(Throws([&] { return sceAt9EncFlush(nullptr, nullptr, 0, nullptr, &internalResult); })); + Require(Throws([&] { return sceAt9EncQueryMemSize(nullptr, &memSize, &internalResult); })); + + Require(memSize == 0); + Require(internalResult == 0); +} diff --git a/core/libs/tests/GuestAtoi.cpp b/core/libs/tests/GuestAtoi.cpp index 0c4d77ddfa..31d63e6ccd 100644 --- a/core/libs/tests/GuestAtoi.cpp +++ b/core/libs/tests/GuestAtoi.cpp @@ -35,10 +35,10 @@ int main() { {" \t+4294967297tail", 1, SavedError}, {"9223372036854775807", -1, SavedError}, {"-9223372036854775808", 0, SavedError}, - {"9223372036854775808", -1, 34}, - {"-9223372036854775809", 0, 34}, + {"9223372036854775808", 0, SavedError}, + {"-9223372036854775809", -1, SavedError}, {"999999999999999999999999999999999999", -1, 34}, - {"-999999999999999999999999999999999999", 0, 34}, + {"-999999999999999999999999999999999999", -1, 34}, }; bool correct = true; for (const auto& test : cases) { diff --git a/core/libs/tests/GuestAudio3dMixer.cpp b/core/libs/tests/GuestAudio3dMixer.cpp new file mode 100644 index 0000000000..c7b09af09c --- /dev/null +++ b/core/libs/tests/GuestAudio3dMixer.cpp @@ -0,0 +1,132 @@ +#include "Mixer.hpp" + +#include +#include +#include +#include + +static void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +int main() { + std::int16_t pcm[4] = {16384, -16384, 8192, -8192}; + Audio3dInternal::Source s{pcm, 2, 2, false, 1.0f, 0.0f}; + std::vector out; + Audio3dInternal::MixFrame(&s, 1, 2, out); + Require(out.size() == 4, "frame is granularity*2 samples"); + Require(out[0] == 16384 && out[1] == -16384, "centered stereo passes through"); + Require(out[2] == 8192 && out[3] == -8192, "centered stereo passes through the second frame"); + + std::int16_t panned[2] = {10000, 20000}; + Audio3dInternal::Source hard_right{panned, 1, 2, false, 1.0f, 1.0f}; + std::vector panned_out; + Audio3dInternal::MixFrame(&hard_right, 1, 1, panned_out); + Require(panned_out.size() == 2 && panned_out[0] == 0 && panned_out[1] == 19999, "hard right keeps only the right channel"); + + float floats[2] = {0.5f, -0.5f}; + Audio3dInternal::Source float_source{floats, 1, 2, true, 1.0f, 0.0f}; + std::vector float_out; + Audio3dInternal::MixFrame(&float_source, 1, 1, float_out); + Require(float_out.size() == 2 && float_out[0] == 16384 && float_out[1] == -16384, "float samples are converted"); + + std::int16_t mono[1] = {10000}; + Audio3dInternal::Source mono_source{mono, 1, 1, false, 1.0f, 0.0f}; + std::vector mono_out; + Audio3dInternal::MixFrame(&mono_source, 1, 1, mono_out); + Require(mono_out.size() == 2 && mono_out[0] == 10000 && mono_out[1] == 10000, "centered mono is duplicated"); + + std::vector short_out; + Audio3dInternal::MixFrame(&mono_source, 1, 4, short_out); + Require(short_out.size() == 8, "short frame keeps the full frame size"); + Require(short_out[0] == 10000 && short_out[1] == 10000, "short frame mixes the first sample"); + Require(short_out[2] == 0 && short_out[3] == 0 && short_out[4] == 0 && short_out[5] == 0 && short_out[6] == 0 && short_out[7] == 0, "short frame zero pads the tail"); + + std::int16_t loud[1] = {32767}; + Audio3dInternal::Source loud_source{loud, 1, 1, false, 1.0f, 0.0f}; + Audio3dInternal::Source loud_sources[2] = {loud_source, loud_source}; + std::vector loud_out; + Audio3dInternal::MixFrame(loud_sources, 2, 1, loud_out); + Require(loud_out.size() == 2 && loud_out[0] == 32767 && loud_out[1] == 32767, "sums above full scale clamp"); + + Audio3dInternal::Source out_of_range{panned, 1, 2, false, 1.0f, 2.0f}; + std::vector out_of_range_out; + Audio3dInternal::MixFrame(&out_of_range, 1, 1, out_of_range_out); + Require(out_of_range_out.size() == 2 && out_of_range_out[0] == 0 && out_of_range_out[1] == 19999, "out of range pan clamps"); + + std::vector empty_out; + Audio3dInternal::MixFrame(nullptr, 0, 2, empty_out); + Require(empty_out.size() == 4 && empty_out[0] == 0 && empty_out[1] == 0 && empty_out[2] == 0 && empty_out[3] == 0, "no sources produce silence"); + + std::int16_t skipped[2] = {10000, 10000}; + Audio3dInternal::Source null_samples{nullptr, 1, 2, false, 1.0f, 0.0f}; + Audio3dInternal::Source empty_samples{skipped, 0, 2, false, 1.0f, 0.0f}; + Audio3dInternal::Source skipped_sources[2] = {null_samples, empty_samples}; + std::vector skipped_out; + Audio3dInternal::MixFrame(skipped_sources, 2, 1, skipped_out); + Require(skipped_out.size() == 2 && skipped_out[0] == 0 && skipped_out[1] == 0, "null and empty sources are skipped"); + + std::int16_t surround6[6] = {1000, 2000, 3000, 0, 4000, 5000}; + Audio3dInternal::Source surround6_source{surround6, 1, 6, false, 1.0f, 0.0f}; + std::vector surround6_out; + Audio3dInternal::MixFrame(&surround6_source, 1, 1, surround6_out); + Require(surround6_out.size() == 2 && surround6_out[0] == 5950 && surround6_out[1] == 7657, + "6 channel downmix folds center and surrounds, dropping LFE"); + + std::int16_t surround8[8] = {1000, 2000, 3000, 0, 4000, 5000, 6000, 7000}; + Audio3dInternal::Source surround8_source{surround8, 1, 8, false, 1.0f, 0.0f}; + std::vector surround8_out; + Audio3dInternal::MixFrame(&surround8_source, 1, 1, surround8_out); + Require(surround8_out.size() == 2 && surround8_out[0] == 10192 && surround8_out[1] == 12606, + "8 channel downmix folds rear surrounds into stereo"); + + std::int16_t surround5[5] = {1000, 2000, 3000, 0, 4000}; + Audio3dInternal::Source surround5_source{surround5, 1, 5, false, 1.0f, 0.0f}; + std::vector surround5_out; + Audio3dInternal::MixFrame(&surround5_source, 1, 1, surround5_out); + Require(surround5_out.size() == 2 && surround5_out[0] == 5950 && surround5_out[1] == 4121, + "5 channel downmix reads only the channels present"); + + std::int16_t surround3[3] = {1000, 2000, 3000}; + Audio3dInternal::Source surround3_source{surround3, 1, 3, false, 1.0f, 0.0f}; + std::vector surround3_out; + Audio3dInternal::MixFrame(&surround3_source, 1, 1, surround3_out); + Require(surround3_out.size() == 2 && surround3_out[0] == 3121 && surround3_out[1] == 4121, + "3 channel downmix folds center into both sides"); + + std::int16_t surround4[4] = {1000, 2000, 3000, 0}; + Audio3dInternal::Source surround4_source{surround4, 1, 4, false, 1.0f, 0.0f}; + std::vector surround4_out; + Audio3dInternal::MixFrame(&surround4_source, 1, 1, surround4_out); + Require(surround4_out.size() == 2 && surround4_out[0] == 3121 && surround4_out[1] == 4121, + "4 channel downmix drops LFE"); + + std::int16_t surround6_panned[6] = {1000, 2000, 3000, 0, 4000, 5000}; + Audio3dInternal::Source surround6_panned_source{surround6_panned, 1, 6, false, 1.0f, 1.0f}; + std::vector surround6_panned_out; + Audio3dInternal::MixFrame(&surround6_panned_source, 1, 1, surround6_panned_out); + Require(surround6_panned_out.size() == 2 && surround6_panned_out[0] == 0 && surround6_panned_out[1] == 7657, + "6 channel downmix is panned after folding"); + + Audio3dInternal::Source hard_left{panned, 1, 2, false, 1.0f, -1.0f}; + std::vector left_out; + Audio3dInternal::MixFrame(&hard_left, 1, 1, left_out); + Require(left_out.size() == 2 && left_out[0] == 10000 && left_out[1] == 0, + "hard left pan keeps only the left channel"); + + Audio3dInternal::Source zero_gain{panned, 1, 2, false, 0.0f, 0.0f}; + std::vector zero_gain_out; + Audio3dInternal::MixFrame(&zero_gain, 1, 1, zero_gain_out); + Require(zero_gain_out.size() == 2 && zero_gain_out[0] == 0 && zero_gain_out[1] == 0, + "zero gain produces silence"); + + Audio3dInternal::Source half_gain{panned, 1, 2, false, 0.5f, 0.0f}; + std::vector half_gain_out; + Audio3dInternal::MixFrame(&half_gain, 1, 1, half_gain_out); + Require(half_gain_out.size() == 2 && half_gain_out[0] == 5000 && half_gain_out[1] == 10000, + "half gain scales stereo samples by half"); + + return 0; +} diff --git a/core/libs/tests/GuestAudio3dPort.cpp b/core/libs/tests/GuestAudio3dPort.cpp index 6c754ac77a..5b5467437d 100644 --- a/core/libs/tests/GuestAudio3dPort.cpp +++ b/core/libs/tests/GuestAudio3dPort.cpp @@ -7,6 +7,7 @@ #include #include #include +#include extern "C" { void APS5_VABI sceAudio3dGetDefaultOpenParameters(Audio3dOpenParameters* parameters); @@ -23,6 +24,12 @@ int APS5_VABI sceAudio3dPortFlush(std::uint32_t port_id); int APS5_VABI sceAudio3dObjectReserve(std::uint32_t port_id, std::uint32_t* object_id); int APS5_VABI sceAudio3dObjectUnreserve(std::uint32_t port_id, std::uint32_t object_id); int APS5_VABI sceAudio3dObjectSetAttributes(std::uint32_t port_id, std::uint32_t object_id, std::uint64_t num_attributes, const Audio3dAttribute* attribute_array); +int APS5_VABI sceAudio3dBedWrite(std::uint32_t port_id, std::uint32_t num_channels, std::uint32_t format, const void* buffer, std::uint32_t num_samples); +int APS5_VABI sceAudio3dBedWrite2(std::uint32_t port_id, std::uint32_t num_channels, std::uint32_t format, const void* buffer, std::uint32_t num_samples, std::uint32_t output_route, bool restricted); +int APS5_VABI sceAudio3dAudioOutOpen(std::uint32_t port_id, int user_id, std::int32_t type, std::int32_t index, std::uint32_t len, std::uint32_t freq, std::uint32_t param); +int APS5_VABI sceAudio3dAudioOutClose(std::int32_t handle); +int APS5_VABI sceAudio3dAudioOutOutput(std::int32_t handle, const void* ptr); +int APS5_VABI sceAudio3dAudioOutOutputs(AudioOutOutputParam* param, std::uint32_t num); } namespace { @@ -195,11 +202,8 @@ void CheckObjects() { } void CheckQueue() { - using namespace std::chrono; - constexpr std::uint32_t granularity = 0x1800; - constexpr auto frame = microseconds(1000000ull * granularity / 48000); Audio3dOpenParameters parameters = Defaults(); - parameters.granularity = granularity; + parameters.granularity = 0x1800; std::uint32_t id = 7; Require(Open(parameters, &id) == 0 && id == 0, "open"); CheckAttributes(); @@ -213,23 +217,53 @@ void CheckQueue() { RequireLevel(1, 1, "one frame queued"); Require(sceAudio3dPortAdvance(0) == 0, "advance 2"); RequireLevel(2, 0, "queue full"); - RequireThrows([] { sceAudio3dPortAdvance(0); }, "advance into a full queue must throw"); + Require(sceAudio3dPortAdvance(0) == NOT_READY, "advance into a full queue must be NOT_READY"); RequireThrows([] { sceAudio3dPortPush(0, 2); }, "unknown blocking mode must throw"); - - auto start = steady_clock::now(); - Require(sceAudio3dPortPush(0, 1) == 0, "blocking push"); - auto waited = steady_clock::now() - start; - Require(waited >= frame - milliseconds(2), "blocking push waits until one frame has played"); - RequireLevel(1, 1, "blocking push returns with one free slot"); - + Require(sceAudio3dPortPush(0, 1) == 0, "sync push"); + RequireLevel(0, 2, "sync push drains every queued frame"); Require(sceAudio3dPortAdvance(0) == 0, "advance 3"); - start = steady_clock::now(); + Require(sceAudio3dPortAdvance(0) == 0, "advance 4"); Require(sceAudio3dPortPush(0, 0) == 0, "async push"); - Require(steady_clock::now() - start < frame, "async push does not wait for a frame"); - std::uint32_t level = 0; - Require(sceAudio3dPortGetQueueLevel(0, &level, nullptr) == 0 && level >= 1, "async push keeps frames queued"); - std::this_thread::sleep_for(frame * 3); - RequireLevel(0, 2, "frames drain at the port's rate"); + RequireLevel(1, 1, "async push keeps the queue one below the depth"); + Require(sceAudio3dPortClose(0) == 0, "close"); +} + +void CheckMixAndPush() { + Audio3dOpenParameters parameters = Defaults(); + std::uint32_t id = 7; + Require(Open(parameters, &id) == 0 && id == 0, "open"); + Require(sceAudio3dPortAdvance(0) == 0, "advance mixes an empty bed into silence"); + RequireLevel(1, 1, "the silent frame is queued"); + const std::int16_t samples[8] = {1000, -1000, 2000, -2000, 3000, -3000, 4000, -4000}; + Require(sceAudio3dBedWrite(0, 2, 0, samples, 4) == 0, "bed write"); + Require(sceAudio3dPortAdvance(0) == 0, "advance mixes the bed"); + RequireLevel(2, 0, "the mixed frame is queued"); + Require(sceAudio3dPortAdvance(0) == NOT_READY, "a full queue rejects the mix"); + Require(sceAudio3dPortPush(0, 1) == 0, "sync push"); + RequireLevel(0, 2, "sync push drains every queued frame"); + Require(sceAudio3dPortAdvance(0) == 0, "advance after the sync drain"); + Require(sceAudio3dPortAdvance(0) == 0, "advance fills the queue again"); + Require(sceAudio3dPortPush(0, 0) == 0, "async push"); + RequireLevel(1, 1, "async push leaves the queue one below the depth"); + Require(sceAudio3dPortClose(0) == 0, "close"); +} + +void CheckPushKeepsRealTime() { + Audio3dOpenParameters parameters = Defaults(); + std::uint32_t id = 7; + Require(Open(parameters, &id) == 0 && id == 0, "open"); + const std::int16_t samples[8] = {1000, -1000, 2000, -2000, 3000, -3000, 4000, -4000}; + Require(sceAudio3dBedWrite(0, 2, 0, samples, 4) == 0, "bed write"); + constexpr std::uint32_t frames = 200; + const std::uint64_t blockUs = 1000000ull * parameters.granularity / 48000ull; + const auto start = std::chrono::steady_clock::now(); + for (std::uint32_t i = 0; i < frames; i++) { + Require(sceAudio3dPortAdvance(0) == 0, "advance"); + Require(sceAudio3dPortPush(0, 1) == 0, "sync push"); + } + const auto elapsed = std::chrono::duration_cast(std::chrono::steady_clock::now() - start).count(); + const std::uint64_t expected = frames * blockUs; + Require(elapsed + 150000 >= static_cast(expected), "a blocking push must run at real time"); Require(sceAudio3dPortClose(0) == 0, "close"); } @@ -246,6 +280,38 @@ void CheckFlush() { Require(sceAudio3dPortClose(0) == 0, "close"); } +void CheckAudioOutPorts() { + const std::uint32_t granularity = Defaults().granularity; + Require(sceAudio3dAudioOutOpen(0, SYSTEM_USER, 126, 0, granularity, 48000, 1) == INVALID_PORT, "audio out open needs an open port"); + Audio3dOpenParameters parameters = Defaults(); + std::uint32_t id = 7; + Require(Open(parameters, &id) == 0 && id == 0, "open"); + Require(sceAudio3dAudioOutOpen(1, SYSTEM_USER, 126, 0, granularity, 48000, 1) == INVALID_PORT, "audio out open on port 1"); + Require(sceAudio3dAudioOutOpen(0, SYSTEM_USER, 126, 0, granularity + 0x100, 48000, 1) == INVALID_PARAMETER, "len must equal the granularity"); + Require(sceAudio3dAudioOutOpen(0, SYSTEM_USER, 126, 1, granularity, 48000, 1) == INVALID_PARAMETER, "index must be 0"); + Require(sceAudio3dAudioOutOpen(0, SYSTEM_USER, 126, 0, granularity, 48000, 8) == INVALID_PARAMETER, "unknown format"); + const int handle = sceAudio3dAudioOutOpen(0, SYSTEM_USER, 126, 0, granularity, 48000, 1); + Require(handle > 0, "audio out open returns a handle"); + const std::vector block(static_cast(granularity) * 2, 256); + for (std::uint32_t i = 0; i < parameters.queue_depth; ++i) { + Require(sceAudio3dAudioOutOutput(handle, block.data()) == static_cast(granularity), "audio out output returns the sample count"); + } + Require(sceAudio3dAudioOutOutput(handle, block.data()) == NOT_READY, "a full pending queue must be NOT_READY"); + Require(sceAudio3dPortPush(0, 1) == 0, "sync push drains the associated ports"); + Require(sceAudio3dAudioOutOutput(handle, block.data()) == static_cast(granularity), "push drains the pending queue"); + Require(sceAudio3dPortFlush(0) == 0, "flush drains the associated ports"); + Require(sceAudio3dAudioOutOutput(handle, block.data()) == static_cast(granularity), "flush resets the pending queue"); + Require(sceAudio3dAudioOutOutputs(nullptr, 0) == INVALID_PARAMETER, "outputs rejects an empty batch"); + Require(sceAudio3dAudioOutClose(handle) == 0, "audio out close"); + Require(sceAudio3dAudioOutClose(handle) == INVALID_PORT, "audio out double close"); + Require(sceAudio3dAudioOutOutput(handle, block.data()) == INVALID_PORT, "output on a closed handle"); + const int open_handle = sceAudio3dAudioOutOpen(0, SYSTEM_USER, 126, 0, granularity, 48000, 1); + Require(open_handle > 0, "audio out reopen"); + Require(sceAudio3dPortClose(0) == 0, "close"); + Require(sceAudio3dAudioOutOutput(open_handle, block.data()) == INVALID_PORT, "port close clears the associated ports"); + Require(sceAudio3dAudioOutClose(open_handle) == INVALID_PORT, "port close closes the associated ports"); +} + void CheckObjectAttributes() { float value = 0.5f; Audio3dAttribute attribute{0x10001, 0, &value, sizeof(value)}; @@ -268,6 +334,62 @@ void CheckObjectAttributes() { Require(sceAudio3dPortClose(0) == 0, "close"); } +void CheckBed() { + const std::int16_t samples[8] = {}; + Require(sceAudio3dBedWrite(0, 2, 0, samples, 4) == INVALID_PORT, "bed write needs an open port"); + Require(sceAudio3dBedWrite2(0, 2, 0, samples, 4, 0, false) == INVALID_PORT, "bed write2 needs an open port"); + Audio3dOpenParameters parameters = Defaults(); + std::uint32_t id = 7; + Require(Open(parameters, &id) == 0 && id == 0, "open"); + const auto consume = [] { + Require(sceAudio3dPortAdvance(0) == 0, "advance after a bed write"); + Require(sceAudio3dPortFlush(0) == 0, "flush the mixed frame after a bed write"); + }; + Require(sceAudio3dBedWrite(0, 2, 0, samples, 4) == 0, "2 channel s16 bed write"); + consume(); + Require(sceAudio3dBedWrite(0, 1, 0, samples, 4) == INVALID_PARAMETER, "1 channel bed write"); + Require(sceAudio3dBedWrite(0, 2, 0, nullptr, 4) == INVALID_PARAMETER, "null bed buffer"); + Require(sceAudio3dBedWrite2(0, 2, 0, samples, 4, 0, false) == 0, "bed write2 route both"); + consume(); + Require(sceAudio3dBedWrite2(0, 2, 0, samples, 4, 1, false) == 0, "bed write2 route hmu only"); + consume(); + Require(sceAudio3dBedWrite2(0, 2, 0, samples, 4, 2, false) == 0, "bed write2 route tv only"); + consume(); + Require(sceAudio3dBedWrite2(0, 2, 0, samples, 4, 3, false) == INVALID_PARAMETER, "bed write2 unknown route"); + Require(sceAudio3dBedWrite(0, 2, 2, samples, 4) == INVALID_PARAMETER, "unknown bed format"); + Require(sceAudio3dBedWrite(0, 2, 0, samples, 0) == INVALID_PARAMETER, "zero bed samples"); + Require(sceAudio3dBedWrite(0, 2, 0, reinterpret_cast(samples) + 1, 4) == INVALID_PARAMETER, "misaligned s16 bed buffer"); + const float floats[8] = {}; + Require(sceAudio3dBedWrite(0, 2, 1, floats, 4) == 0, "float bed write"); + consume(); + const std::uint32_t granularity = parameters.granularity; + std::vector wide(static_cast(granularity) * 8, 0); + Require(sceAudio3dBedWrite(0, 6, 0, wide.data(), granularity) == 0, "6 channel bed write"); + consume(); + Require(sceAudio3dBedWrite(0, 8, 0, wide.data(), granularity) == 0, "8 channel bed write"); + consume(); + Require(sceAudio3dPortClose(0) == 0, "close"); +} + +void CheckBedQueue() { + Audio3dOpenParameters parameters = Defaults(); + std::uint32_t id = 7; + Require(Open(parameters, &id) == 0 && id == 0, "open"); + const std::int16_t samples[8] = {1000, -1000, 2000, -2000, 3000, -3000, 4000, -4000}; + for (std::uint32_t i = 0; i < parameters.queue_depth; ++i) { + Require(sceAudio3dBedWrite(0, 2, 0, samples, 4) == 0, "each bed write fills the queue"); + } + Require(sceAudio3dBedWrite(0, 2, 0, samples, 4) == NOT_READY, "a full bed queue must be NOT_READY"); + Require(sceAudio3dPortAdvance(0) == 0, "advance consumes one bed"); + Require(sceAudio3dBedWrite(0, 2, 0, samples, 4) == 0, "the consumed bed makes room"); + Require(sceAudio3dPortAdvance(0) == 0, "advance consumes the next bed"); + RequireLevel(2, 0, "two frames are queued from the queued beds"); + Require(sceAudio3dPortFlush(0) == 0, "flush"); + Require(sceAudio3dPortAdvance(0) == 0, "advance with no bed left mixes silence"); + RequireLevel(1, 1, "silence is queued behind the flushed frames"); + Require(sceAudio3dPortClose(0) == 0, "close"); +} + void CheckTerminate() { std::uint32_t id = 7; const Audio3dOpenParameters parameters = Defaults(); @@ -292,7 +414,12 @@ int main() { CheckOpenClose(); CheckObjects(); CheckQueue(); + CheckMixAndPush(); + CheckPushKeepsRealTime(); CheckFlush(); + CheckAudioOutPorts(); CheckObjectAttributes(); + CheckBed(); + CheckBedQueue(); CheckTerminate(); } 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/GuestAudioPropagation.cpp b/core/libs/tests/GuestAudioPropagation.cpp index 3cecb65563..c4d112d342 100644 --- a/core/libs/tests/GuestAudioPropagation.cpp +++ b/core/libs/tests/GuestAudioPropagation.cpp @@ -149,20 +149,16 @@ int main() { Require(sceAudioPropagationSourceCreate(system, &secondSource) == 0, "second source create failed"); float outputs[2][64]; - const auto fill = [&] { std::memset(outputs, 0xab, sizeof(outputs)); }; - const auto zeroed = [&](int index) { - for (const float sample : outputs[index]) { - if (sample != 0.0f) return false; + float expected[2][64]; + const auto fill = [&] { + for (std::size_t channel = 0; channel < 2; ++channel) { + for (std::size_t sample = 0; sample < 64; ++sample) { + outputs[channel][sample] = static_cast(sample + 1) / (channel == 0 ? 128.0f : -128.0f); + } } - return true; - }; - const auto untouched = [&] { - const auto* bytes = reinterpret_cast(outputs); - for (std::size_t index = 0; index < sizeof(outputs); ++index) { - if (bytes[index] != 0xab) return false; - } - return true; + std::memcpy(expected, outputs, sizeof(outputs)); }; + const auto untouched = [&] { return std::memcmp(outputs, expected, sizeof(outputs)) == 0; }; AudioPropagation::RenderInfo infos[2]{}; const auto reset = [&] { infos[0] = {{AudioPropagation::RenderInfoId, AudioPropagation::RenderInfoSize}, source, outputs[0], sizeof(outputs[0]), AudioPropagation::RenderFormat, 0}; @@ -171,13 +167,26 @@ int main() { }; reset(); Require(sceAudioPropagationSourceRender(system, infos, 1) == 0, "render failed"); - Require(zeroed(0), "render left the output unwritten"); - const auto* tail = reinterpret_cast(outputs[1]); - Require(tail[0] == 0xab, "render wrote past the output size"); + Require(untouched(), "empty source erased an existing mix"); reset(); infos[0].outputSize = sizeof(float) * 16; Require(sceAudioPropagationSourceRender(system, infos, 2) == 0, "render of two sources failed"); - Require(outputs[0][15] == 0.0f && outputs[0][16] != 0.0f && zeroed(1), "render did not write each output for its size"); + Require(untouched(), "empty sources modified separate destination mixes or their tails"); + reset(); + infos[1].output = outputs[0]; + Require(sceAudioPropagationSourceRender(system, infos, 2) == 0, "render into a shared mix failed"); + Require(untouched(), "empty sources modified a shared destination mix"); + reset(); + infos[0].outputSize = sizeof(float) * 32; + infos[1].output = outputs[0] + 16; + infos[1].outputSize = sizeof(float) * 32; + Require(sceAudioPropagationSourceRender(system, infos, 2) == 0, "render into overlapping mixes failed"); + Require(untouched(), "empty sources modified overlapping destination mixes"); + reset(); + std::memset(outputs, 0, sizeof(outputs)); + std::memcpy(expected, outputs, sizeof(outputs)); + Require(sceAudioPropagationSourceRender(system, infos, 2) == 0, "render into silence failed"); + Require(untouched(), "empty sources modified a silent destination mix"); reset(); RequireThrows([&] { sceAudioPropagationSourceRender(system, infos, 0); }, "render without infos"); diff --git a/core/libs/tests/GuestAudiodec.cpp b/core/libs/tests/GuestAudiodec.cpp index 5e9ee471f1..404e7fce05 100644 --- a/core/libs/tests/GuestAudiodec.cpp +++ b/core/libs/tests/GuestAudiodec.cpp @@ -321,7 +321,7 @@ void TestHeAac(const std::uint8_t* stream, std::size_t streamSize, std::uint32_t } void TestMp3() { - AudiodecParamMp3 param{sizeof(param), 2}; + AudiodecParamMp3 param{sizeof(param), 1}; AudiodecMp3Info info{}; info.ui_size = sizeof(info); AudiodecCtrl ctrl{¶m, &info, nullptr, nullptr}; @@ -334,9 +334,9 @@ void TestMp3() { while (offset < sizeof(MP3_MONO)) { Require(Decode(handle, ctrl, streams, MP3_MONO + offset, sizeof(MP3_MONO) - offset) == 0); Require(streams.au.ui_au_size == 144 * 32000 / 48000 + (MP3_MONO[offset + 2] >> 1 & 1)); - Require(streams.pcm.ui_pcm_size == 1152 * 4); + Require(streams.pcm.ui_pcm_size == 1152 * 2); Require(info.ui_header >> 21 == 0x7FF && info.uc_mode == 3 && info.i_result == 0); - for (std::size_t i = 0; i < 1152; ++i) peak = std::fmax(peak, std::fabs(reinterpret_cast(streams.output.data())[i])); + for (std::size_t i = 0; i < 1152; ++i) peak = std::fmax(peak, std::fabs(reinterpret_cast(streams.output.data())[i] / 32768.0f)); offset += streams.au.ui_au_size; ++frames; } @@ -361,6 +361,14 @@ void TestAt9() { Require(Decode(handle, ctrl, streams, silence.data(), silence.size(), 4095) == errorInvalidPcmSize); Require(Decode(handle, ctrl, streams, silence.data(), silence.size()) == errorApiFail); Require(sceAudiodecDeleteDecoder(handle) == 0); + for (const std::int32_t wordSize : {0, 3, -1}) { + param.i_bw_pcm = wordSize; + Require(sceAudiodecCreateDecoder(&ctrl, 1) == errorInvalidWordLength); + } + param.i_bw_pcm = 2; + const std::int32_t floatHandle = sceAudiodecCreateDecoder(&ctrl, 1); + Require(floatHandle > 0 && sceAudiodecDeleteDecoder(floatHandle) == 0); + param.i_bw_pcm = 1; param.ui_config_data[0] = 0xFD; Require(sceAudiodecCreateDecoder(&ctrl, 1) == errorInvalidAt9Config); } @@ -385,13 +393,23 @@ int main() { mp3Info.ui_size = 16; Require(sceAudiodecCreateDecoder(&ctrl, 2) == errorInvalidBsiInfoSize); mp3Info.ui_size = sizeof(mp3Info); - mp3.i_bw_pcm = 3; - Require(sceAudiodecCreateDecoder(&ctrl, 2) == errorInvalidWordLength); + for (const std::int32_t wordSize : {0, 2, 3, -1}) { + mp3.i_bw_pcm = wordSize; + Require(sceAudiodecCreateDecoder(&ctrl, 2) == errorInvalidWordLength); + } mp3.i_bw_pcm = 1; AudiodecParamM4aac aac{sizeof(aac), 1, 3, 3, 2, 0}; AudiodecM4aacInfo aacInfo{sizeof(aacInfo), 0, 0, 0, 0}; AudiodecCtrl aacCtrl{&aac, &aacInfo, nullptr, nullptr}; Require(sceAudiodecCreateDecoder(&aacCtrl, 3) == errorInvalidConfigNumber); + aac.ui_config_number = 1; + for (const std::int32_t wordSize : {0, 3, -1}) { + aac.i_bw_pcm = wordSize; + Require(sceAudiodecCreateDecoder(&aacCtrl, 3) == errorInvalidWordLength); + } + aac.i_bw_pcm = 2; + const std::int32_t floatAac = sceAudiodecCreateDecoder(&aacCtrl, 3); + Require(floatAac > 0 && sceAudiodecDeleteDecoder(floatAac) == 0); const std::int32_t handle = sceAudiodecCreateDecoder(&ctrl, 2); Require(handle > 0); diff --git a/core/libs/tests/GuestAvPlayer.cpp b/core/libs/tests/GuestAvPlayer.cpp index 5e4b500e60..efa5ff1e00 100644 --- a/core/libs/tests/GuestAvPlayer.cpp +++ b/core/libs/tests/GuestAvPlayer.cpp @@ -22,6 +22,7 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceAvPlayer/include/AvPlayer.hpp" extern "C" { AvPlayerInternal* APS5_VABI sceAvPlayerInit(AvPlayerInitData*); @@ -74,6 +75,7 @@ constexpr std::int32_t EventPause = 4; constexpr std::int32_t EventWarning = 0x20; const char* const MoviePath = "app0/avplayer.mp4"; const char* const WebmPath = "app0/avplayer.webm"; +constexpr int WebmVideoFrames = 6; constexpr std::array Movie{ 0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6f, 0x6d, 0x00, 0x00, 0x02, 0x00, @@ -864,7 +866,7 @@ void TestPlayback() { last = frame.timestamp; ++frames; } - return frames >= 8 && sounds >= 8; + return frames >= 2 && sounds >= 2; }), "playback stalled"); Check(sceAvPlayerCurrentTime(player) > 0, "clock not running"); Check(sceAvPlayerSetTrickSpeed(player, 0) == InvalidParams, "zero trick speed accepted"); @@ -958,38 +960,48 @@ void TestVp9OpusPlayback() { file.write(reinterpret_cast(WebmMovie.data()), static_cast(WebmMovie.size())); Check(static_cast(file), "cannot write the WebM movie"); } - Events events; - AvPlayerInitData init = InitData(&events); - auto* player = sceAvPlayerInit(&init); - Check(player != nullptr, "WebM init failed"); - Check(sceAvPlayerAddSource(player, "/app0/avplayer.webm") == 0, "WebM add source failed"); - Check(WaitFor([&] { return events.Seen(EventReady); }), "WebM ready event missing"); - Check(sceAvPlayerStreamCount(player) == 2, "unexpected WebM stream count"); - AvPlayerStreamInfo video{}; - Check(sceAvPlayerGetStreamInfo(player, 0, &video) == 0 && video.type == 1 && video.details.video.width == 112 && video.details.video.height == 64, "unexpected VP9 stream"); - AvPlayerStreamInfo audio{}; - Check(sceAvPlayerGetStreamInfo(player, 1, &audio) == 0 && audio.type == 0 && audio.details.audio.channel_count == 2 && audio.details.audio.sample_rate == SampleRate, "unexpected Opus stream"); - Check(sceAvPlayerEnableStream(player, 0) == 0 && sceAvPlayerEnableStream(player, 1) == 0, "enable WebM streams failed"); - Check(sceAvPlayerStart(player) == 0, "WebM start failed"); - AvPlayerFrameInfoEx frame{}; - AvPlayerFrameInfo sound{}; - int frames = 0; - int sounds = 0; - Check(WaitFor([&] { - if (sceAvPlayerGetAudioData(player, &sound)) { - CheckEnglishAudio(sound); - ++sounds; - } - if (sceAvPlayerGetVideoDataEx(player, &frame)) { - CheckVideoFrame(frame); - ++frames; - } - return !sceAvPlayerIsActive(player); - }), "WebM playback never ended"); - Check(frames >= 5 && sounds >= 2, "WebM delivered " + std::to_string(frames) + " frames and " + std::to_string(sounds) + " audio chunks"); - Check(sceAvPlayerClose(player) == 0, "WebM close failed"); - std::lock_guard lock(allocations.mutex); - Check(allocations.blocks.empty(), "WebM allocations leaked"); + const auto play = [](bool withAudio) { + Events events; + AvPlayerInitData init = InitData(&events); + auto* player = sceAvPlayerInit(&init); + Check(player != nullptr, "WebM init failed"); + Check(sceAvPlayerAddSource(player, "/app0/avplayer.webm") == 0, "WebM add source failed"); + Check(WaitFor([&] { return events.Seen(EventReady); }), "WebM ready event missing"); + Check(sceAvPlayerStreamCount(player) == 2, "unexpected WebM stream count"); + AvPlayerStreamInfo video{}; + Check(sceAvPlayerGetStreamInfo(player, 0, &video) == 0 && video.type == 1 && video.details.video.width == 112 && video.details.video.height == 64, "unexpected VP9 stream"); + AvPlayerStreamInfo audio{}; + Check(sceAvPlayerGetStreamInfo(player, 1, &audio) == 0 && audio.type == 0 && audio.details.audio.channel_count == 2 && audio.details.audio.sample_rate == SampleRate, "unexpected Opus stream"); + Check(sceAvPlayerEnableStream(player, 0) == 0 && (!withAudio || sceAvPlayerEnableStream(player, 1) == 0), "enable WebM streams failed"); + if (!withAudio) Check(sceAvPlayerSetAvSyncMode(player, 1) == 0, "WebM no-sync mode rejected"); + Check(sceAvPlayerStart(player) == 0, "WebM start failed"); + AvPlayerFrameInfoEx frame{}; + AvPlayerFrameInfo sound{}; + int frames = 0; + int sounds = 0; + std::uint64_t last = 0; + Check(WaitFor([&] { + if (withAudio && sceAvPlayerGetAudioData(player, &sound)) { + CheckEnglishAudio(sound); + ++sounds; + } + if (sceAvPlayerGetVideoDataEx(player, &frame)) { + CheckVideoFrame(frame); + Check(frames == 0 || frame.timestamp > last, "WebM video timestamps do not increase"); + last = frame.timestamp; + ++frames; + } + return !sceAvPlayerIsActive(player); + }), "WebM playback never ended"); + Check(sceAvPlayerClose(player) == 0, "WebM close failed"); + std::lock_guard lock(allocations.mutex); + Check(allocations.blocks.empty(), "WebM allocations leaked"); + return std::pair{frames, sounds}; + }; + const auto [decoded, silent] = play(false); + Check(decoded == WebmVideoFrames && silent == 0, "WebM video-only playback delivered " + std::to_string(decoded) + " of " + std::to_string(WebmVideoFrames) + " frames"); + const auto [frames, sounds] = play(true); + Check(frames >= 1 && sounds >= 2, "WebM delivered " + std::to_string(frames) + " frames and " + std::to_string(sounds) + " audio chunks"); } void TestChangeStream() { @@ -1262,6 +1274,26 @@ void TestRestartAfterPausedJumpKeepsClockRunning() { Check(sceAvPlayerClose(player) == 0, "close failed"); } +void TestPausedPreparationAfterAudioStarts(std::uint32_t syncMode) { + Events events; + auto init = InitData(&events); + auto* player = sceAvPlayerInit(&init); + Check(player != nullptr, "init failed"); + Check(sceAvPlayerAddSource(player, "/app0/avplayer.mp4") == 0, "add source failed"); + Check(sceAvPlayerEnableStream(player, VideoStream) == 0, "enable video failed"); + Check(sceAvPlayerEnableStream(player, EnglishAudioStream) == 0, "enable audio failed"); + Check(sceAvPlayerSetAvSyncMode(player, syncMode) == 0, "sync mode failed"); + Check(sceAvPlayerStart(player) == 0, "start failed"); + AvPlayerFrameInfo audio{}; + Check(WaitFor([&] { return sceAvPlayerGetAudioData(player, &audio) != 0; }), "no initial audio frame"); + Check(sceAvPlayerPause(player) == 0, "pause failed"); + AvPlayerFrameInfoEx frame{}; + Check(WaitFor([&] { return sceAvPlayerGetVideoDataEx(player, &frame) != 0; }), "first video frame blocked during paused preparation after audio started"); + CheckVideoFrame(frame); + Check(!sceAvPlayerGetVideoDataEx(player, &frame), "paused preparation delivered more than the initial video frame"); + Check(sceAvPlayerClose(player) == 0, "close failed"); +} + void TestUnsyncedVideoKeepsUpWithAudio() { Events events; AvPlayerInitData init = InitData(&events); @@ -1550,6 +1582,29 @@ void TestCloseFromEventCallback(std::int32_t event) { Check(allocations.blocks.empty(), "callback close leaked media allocations"); } +void TestVideoSyncTolerance() { + Events events; + AvPlayerInitData init = InitData(&events); + auto* player = sceAvPlayerInit(&init); + Check(player != nullptr, "sync tolerance init failed"); + Check(sceAvPlayerAddSource(player, "/app0/avplayer.mp4") == 0, "sync tolerance source failed"); + Check(WaitFor([&] { return events.Seen(EventReady); }), "sync tolerance ready missing"); + Check(sceAvPlayerEnableStream(player, VideoStream) == 0, "sync tolerance video failed"); + Check(sceAvPlayerStart(player) == 0, "sync tolerance start failed"); + Check(WaitFor([&] { return events.Seen(EventPlay); }), "sync tolerance play missing"); + AvPlayerFrameInfoEx frame{}; + Check(WaitFor([&] { return sceAvPlayerGetVideoDataEx(player, &frame) != 0; }), "sync tolerance first frame missing"); + Check(FrameIndex(frame.timestamp) == 0, "first frame not index 0"); + AvPlayer::SetAvPlayerClockForTest_nid_no_patch(player, 5.0); + Check(!sceAvPlayerGetVideoDataEx(player, &frame), "frame beyond sync tolerance delivered prematurely"); + AvPlayer::SetAvPlayerClockForTest_nid_no_patch(player, 10.0); + Check(sceAvPlayerGetVideoDataEx(player, &frame), "frame within sync tolerance was not delivered"); + Check(FrameIndex(frame.timestamp) == 1, "unexpected second frame index"); + Check(sceAvPlayerClose(player) == 0, "sync tolerance close failed"); + std::lock_guard lock(allocations.mutex); + Check(allocations.blocks.empty(), "sync tolerance allocations leaked"); +} + } int main(int argc, char** argv) { @@ -1561,6 +1616,13 @@ int main(int argc, char** argv) { std::puts("AvPlayer callback close tests passed"); return 0; } + if (argc == 2 && std::strcmp(argv[1], "--paused-first-video") == 0) { + WriteMovie(); + TestPausedPreparationAfterAudioStarts(0); + TestPausedPreparationAfterAudioStarts(1); + std::puts("AvPlayer paused first video tests passed"); + return 0; + } Check(argc == 1, "unknown AvPlayer test arguments"); WriteMovie(); TestLoopWarningAtPlayback(); @@ -1581,6 +1643,7 @@ int main(int argc, char** argv) { TestNoSyncVideoCanLeadAudioClock(); TestLoopWarningWaitsForPlayback(); TestEofStopLatency(); + TestVideoSyncTolerance(); std::puts("AvPlayer tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/tests/GuestBacktrace.cpp b/core/libs/tests/GuestBacktrace.cpp index 25e958cdb9..7ae55383e3 100644 --- a/core/libs/tests/GuestBacktrace.cpp +++ b/core/libs/tests/GuestBacktrace.cpp @@ -4,13 +4,20 @@ #include #include #include +#if defined(_WIN32) +#include +#else #include +#endif #include #include extern "C" { int APS5_VABI sceLibcBacktraceGetBufferSize_nid_postfix(int, int*, std::size_t*); int APS5_VABI sceLibcBacktraceSelf_nid_postfix(int, void*, std::size_t, int*); +#if defined(_WIN32) +std::string BacktraceModulePath_nid_no_patch(HMODULE, std::size_t); +#endif } namespace { @@ -63,13 +70,29 @@ std::size_t Used(const std::vector& records) { return total; } +[[gnu::noinline]] std::uintptr_t CurrentIp() { + asm volatile("" ::: "memory"); + return reinterpret_cast(__builtin_return_address(0)); +} + } [[gnu::noinline]] void CheckBacktrace() { +#if defined(_WIN32) + HMODULE handle = nullptr; + Require(GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(&CheckBacktrace), &handle) != 0 && handle != nullptr, + "GetModuleHandleExA of the test failed"); + std::string executable = BacktraceModulePath_nid_no_patch(handle, 1); + const auto slash = executable.find_last_of("/\\"); + Require(!executable.empty() && slash != std::string::npos, "BacktraceModulePath of the test failed"); + executable.erase(0, slash + 1); +#else Dl_info self{}; Require(dladdr(reinterpret_cast(&CheckBacktrace), &self) != 0 && self.dli_fname != nullptr && self.dli_saddr != nullptr, "dladdr of the test failed"); std::string executable = self.dli_fname; executable.erase(0, executable.find_last_of('/') + 1); +#endif int count = -1; std::size_t size = 0; @@ -78,16 +101,26 @@ std::size_t Used(const std::vector& records) { std::vector buffer(size + 64, Fill); int written = -1; - Require(sceLibcBacktraceSelf_nid_postfix(-1, buffer.data(), size, &written) == 0, "Self failed"); + const auto before = CurrentIp(); + const int selfStatus = sceLibcBacktraceSelf_nid_postfix(-1, buffer.data(), size, &written); + const auto after = CurrentIp(); + Require(selfStatus == 0, "Self failed"); Require(written == count, "Self wrote a different count than GetBufferSize reported"); const auto records = Parse(buffer, size, written); Require(Used(records) == size, "Self used a different size than GetBufferSize reported"); for (std::size_t index = size; index < buffer.size(); ++index) Require(buffer[index] == Fill, "Self wrote past the buffer size"); for (const auto& record : records) Require(record.reserved == 0 && !record.module.empty(), "record has a nonzero reserved word or no module"); Require(records.front().module == executable, "first frame is not in the calling module"); +#if defined(_WIN32) + const auto address = reinterpret_cast(handle) + records.front().offset; + const auto callerReturn = reinterpret_cast(handle) + records[1].offset; + Require(address > before && address < after, "first frame is not the caller"); + Require(callerReturn == reinterpret_cast(__builtin_return_address(0)), "second frame is not the caller of CheckBacktrace"); +#else Dl_info caller{}; const auto address = reinterpret_cast(self.dli_fbase) + records.front().offset; Require(dladdr(reinterpret_cast(address), &caller) != 0 && caller.dli_saddr == self.dli_saddr, "first frame is not the caller"); +#endif for (const int limit : {0, 1, 2}) { Require(sceLibcBacktraceGetBufferSize_nid_postfix(limit, &count, &size) == 0 && count == limit, "GetBufferSize ignored the frame limit"); diff --git a/core/libs/tests/GuestBadOptionalAccess.cpp b/core/libs/tests/GuestBadOptionalAccess.cpp new file mode 100644 index 0000000000..a4280e3ef5 --- /dev/null +++ b/core/libs/tests/GuestBadOptionalAccess.cpp @@ -0,0 +1,40 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +struct TypeRecord { const void* const* vtable; const char* name; const TypeRecord* base; }; +struct Object { const void* vtable; const char* message; }; +struct Vtable { std::ptrdiff_t offset; const TypeRecord* type; const void* destroy; const void* deleteObject; const char* (APS5_VABI *what)(const Object*); }; + +extern "C" { +extern const TypeRecord _ZTISt9exception_nid_postfix; +extern const TypeRecord _ZTISt19bad_optional_access_nid_postfix; +extern const Vtable _ZTVSt19bad_optional_access_nid_postfix; +void APS5_VABI _ZNSt19bad_optional_accessC1Ev_nid_postfix(Object*); +void APS5_VABI _ZNSt19bad_optional_accessD1Ev_nid_postfix(Object*); +[[noreturn]] void APS5_VABI _ZSt26_Throw_bad_optional_accessv_nid_postfix(); +} + +static void Require(bool value) { if (!value) std::abort(); } + +int main() { + const auto& type = _ZTISt19bad_optional_access_nid_postfix; + Require(std::strcmp(type.name, "St19bad_optional_access") == 0); + Require(type.base == &_ZTISt9exception_nid_postfix); + const auto& table = _ZTVSt19bad_optional_access_nid_postfix; + Require(table.offset == 0 && table.type == &type && table.what != nullptr); + Object object{}; + _ZNSt19bad_optional_accessC1Ev_nid_postfix(&object); + Require(object.vtable == &table.destroy); + Require(std::strcmp(table.what(&object), "bad_optional_access") == 0); + _ZNSt19bad_optional_accessD1Ev_nid_postfix(&object); + bool thrown = false; + try { + _ZSt26_Throw_bad_optional_accessv_nid_postfix(); + } catch (...) { + thrown = true; + } + Require(thrown); + return 0; +} 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..e118532944 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,35 @@ int main() { threw = true; } Require(threw, "an input report without a device did not throw"); + Require(sceBluetoothHidDebugGetVersion() == 0x1e, "debug get version did not return the firmware version"); + 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/GuestBoundsChecking.cpp b/core/libs/tests/GuestBoundsChecking.cpp new file mode 100644 index 0000000000..4021e99e48 --- /dev/null +++ b/core/libs/tests/GuestBoundsChecking.cpp @@ -0,0 +1,242 @@ +#include "prx/libc/include/FileStream.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include + +using GuestContextCompare = int (APS5_VABI*)(const void*, const void*, void*); + +extern "C" { +void* APS5_VABI set_constraint_handler_s_nid_postfix(void*); +void* APS5_VABI bsearch_s_nid_postfix(const void*, const void*, std::size_t, std::size_t, GuestContextCompare, void*); +std::uint16_t* APS5_VABI wcstok_s_nid_postfix(std::uint16_t*, std::size_t*, const std::uint16_t*, std::uint16_t**); +int APS5_VABI wctomb_s_nid_postfix(int*, char*, std::size_t, std::uint16_t); +int APS5_VABI wcrtomb_s_nid_postfix(std::size_t*, char*, std::size_t, std::uint16_t, void*); +int APS5_VABI mbstowcs_s_nid_postfix(std::size_t*, std::uint16_t*, std::size_t, const char*, std::size_t); +int APS5_VABI mbsrtowcs_s_nid_postfix(std::size_t*, std::uint16_t*, std::size_t, const char**, std::size_t, void*); +char* APS5_VABI gets_s_nid_postfix(char*, std::size_t); +FileStream* APS5_VABI fopen_nid_postfix(const char*, const char*); +int APS5_VABI fclose_nid_postfix(FileStream*); +extern FileStream* __stdinp_nid_postfix; +} + +namespace { + +constexpr std::size_t failed = static_cast(-1); +constexpr std::size_t tooLarge = std::size_t{1} << 61; + +int constraintCalls = 0; +int constraintError = 0; +std::string constraintMessage; + +void APS5_VABI RecordConstraint(const char* message, void* pointer, int error) { + ++constraintCalls; + constraintMessage = message; + constraintError = error; + if (pointer != nullptr) { + std::fprintf(stderr, "constraint handler received a non-null pointer\n"); + std::abort(); + } +} + +void Require(bool value, const char* message) { + if (!value) { + std::fprintf(stderr, "%s\n", message); + std::abort(); + } +} + +void RequireViolation(int before, const char* message, int error) { + if (constraintCalls != before + 1 || constraintMessage != message || constraintError != error) { + std::fprintf(stderr, "expected one \"%s\" violation, got %d calls (last \"%s\")\n", message, constraintCalls - before, constraintMessage.c_str()); + std::abort(); + } +} + +int APS5_VABI CompareInts(const void* left, const void* right, void* context) { + ++*static_cast(context); + const int a = *static_cast(left); + const int b = *static_cast(right); + return (a > b) - (a < b); +} + +std::basic_string Wide(const char* text) { + std::basic_string wide; + while (*text) wide.push_back(static_cast(*text++)); + return wide; +} + +void CheckBsearch() { + const int values[] = {1, 3, 3, 3, 5, 7, 9}; + int calls = 0; + const int key = 3; + Require(bsearch_s_nid_postfix(&key, values, 7, sizeof(int), CompareInts, &calls) == &values[3] && calls == 1, + "bsearch_s did not probe the middle element first"); + const int missing = 4; + calls = 0; + Require(bsearch_s_nid_postfix(&missing, values, 7, sizeof(int), CompareInts, &calls) == nullptr && calls == 3, + "bsearch_s found a missing key"); + const int last = 9; + Require(bsearch_s_nid_postfix(&last, values, 7, sizeof(int), CompareInts, &calls) == &values[6], "bsearch_s missed the last element"); + int before = constraintCalls; + Require(bsearch_s_nid_postfix(nullptr, nullptr, 0, sizeof(int), nullptr, nullptr) == nullptr && constraintCalls == before, + "bsearch_s reported an empty array"); + Require(bsearch_s_nid_postfix(nullptr, values, 7, sizeof(int), CompareInts, &calls) == nullptr, "bsearch_s accepted a null key"); + RequireViolation(before, "bsearch_s: bad argument", 22); + before = constraintCalls; + Require(bsearch_s_nid_postfix(&key, values, 0, tooLarge, CompareInts, &calls) == nullptr, "bsearch_s accepted an oversized element"); + RequireViolation(before, "bsearch_s: bad argument", 22); +} + +void CheckWcstok() { + auto text = Wide(",,ab;c,,;"); + const auto delimiters = Wide(",;"); + std::size_t remaining = text.size() + 1; + std::uint16_t* context = nullptr; + std::uint16_t* token = wcstok_s_nid_postfix(text.data(), &remaining, delimiters.c_str(), &context); + Require(token == text.data() + 2 && token[0] == 'a' && token[1] == 'b' && token[2] == 0 && remaining == 5 && context == text.data() + 5, + "wcstok_s did not return the first token"); + token = wcstok_s_nid_postfix(nullptr, &remaining, delimiters.c_str(), &context); + Require(token == text.data() + 5 && token[1] == 0 && remaining == 3 && context == text.data() + 7, "wcstok_s did not return the second token"); + token = wcstok_s_nid_postfix(nullptr, &remaining, delimiters.c_str(), &context); + Require(token == nullptr && remaining == 1 && context == text.data() + 9, "wcstok_s did not stop at the end of the string"); + + int before = constraintCalls; + auto unterminated = Wide("abc"); + remaining = 2; + Require(wcstok_s_nid_postfix(unterminated.data(), &remaining, delimiters.c_str(), &context) == nullptr, "wcstok_s read past s1max"); + RequireViolation(before, "wcstok_s: missing delimiter", 22); + before = constraintCalls; + auto separators = Wide(",,,x"); + remaining = 3; + Require(wcstok_s_nid_postfix(separators.data(), &remaining, delimiters.c_str(), &context) == nullptr, "wcstok_s skipped past s1max"); + RequireViolation(before, "wcstok_s: missing token", 22); + before = constraintCalls; + remaining = 0; + Require(wcstok_s_nid_postfix(separators.data(), &remaining, delimiters.c_str(), &context) == nullptr, "wcstok_s accepted a zero s1max"); + RequireViolation(before, "wcstok_s: missing token", 22); + before = constraintCalls; + context = nullptr; + remaining = 4; + Require(wcstok_s_nid_postfix(nullptr, &remaining, delimiters.c_str(), &context) == nullptr, "wcstok_s accepted a null context string"); + RequireViolation(before, "wcstok_s: bad argument", 22); +} + +void CheckWideToMultibyte() { + int status = 7; + char bytes[4] = {'x', 'x', 'x', 'x'}; + Require(wctomb_s_nid_postfix(&status, bytes, sizeof(bytes), 'A') == 0 && status == 1 && bytes[0] == 'A', "wctomb_s did not convert"); + Require(wctomb_s_nid_postfix(&status, bytes, sizeof(bytes), 0x263a) == 86 && status == -1, "wctomb_s converted an unrepresentable character"); + Require(wctomb_s_nid_postfix(&status, nullptr, 0, 'A') == 0 && status == 0, "wctomb_s reported state dependence"); + status = 7; + int before = constraintCalls; + Require(wctomb_s_nid_postfix(&status, bytes, 0, 'A') == 34 && status == 7, "wctomb_s wrote into a zero-size buffer"); + RequireViolation(before, "wctomb_s: buffer too small", 34); + before = constraintCalls; + Require(wctomb_s_nid_postfix(nullptr, bytes, sizeof(bytes), 'A') == 22, "wctomb_s accepted a null status"); + RequireViolation(before, "wctomb_s: bad argument", 22); + + unsigned char state[128]{}; + std::size_t result = 7; + Require(wcrtomb_s_nid_postfix(&result, bytes, sizeof(bytes), 'B', state) == 0 && result == 1 && bytes[0] == 'B', "wcrtomb_s did not convert"); + Require(wcrtomb_s_nid_postfix(&result, nullptr, 0, 'B', state) == 0 && result == 1, "wcrtomb_s did not measure the reset sequence"); + Require(wcrtomb_s_nid_postfix(&result, bytes, sizeof(bytes), 0x263a, state) == 86 && result == failed, "wcrtomb_s converted an unrepresentable character"); + before = constraintCalls; + bytes[0] = 'x'; + Require(wcrtomb_s_nid_postfix(&result, bytes, sizeof(bytes), 'B', nullptr) == 22 && result == failed && bytes[0] == 0, "wcrtomb_s accepted a null state"); + RequireViolation(before, "wcrtomb_s: bad argument", 22); + before = constraintCalls; + bytes[0] = 'x'; + Require(wcrtomb_s_nid_postfix(&result, bytes, 0, 'B', state) == 22 && result == failed && bytes[0] == 'x', "wcrtomb_s accepted a zero-size buffer"); + RequireViolation(before, "wcrtomb_s: bad argument", 22); +} + +void CheckMultibyteToWide() { + std::uint16_t wide[8]; + std::size_t result = 0; + Require(mbstowcs_s_nid_postfix(&result, nullptr, 0, "hello", 0) == 0 && result == 5, "mbstowcs_s did not count the characters"); + Require(mbstowcs_s_nid_postfix(&result, wide, 8, "hello", 8) == 0 && result == 5 && wide[0] == 'h' && wide[4] == 'o' && wide[5] == 0, + "mbstowcs_s did not convert the string"); + Require(mbstowcs_s_nid_postfix(&result, wide, 8, "hello", 3) == 0 && result == 3 && wide[2] == 'l' && wide[3] == 0, + "mbstowcs_s did not stop at len"); + wide[0] = 'x'; + Require(mbstowcs_s_nid_postfix(&result, wide, 8, "hello", 0) == 0 && result == 0 && wide[0] == 0, "mbstowcs_s did not handle a zero len"); + int before = constraintCalls; + wide[0] = 'x'; + Require(mbstowcs_s_nid_postfix(&result, wide, 5, "hello", 8) == 34 && result == failed && wide[0] == 0, "mbstowcs_s overflowed the buffer"); + RequireViolation(before, "mbstowcs_s: buffer overflow", 34); + before = constraintCalls; + wide[0] = 'x'; + Require(mbstowcs_s_nid_postfix(&result, wide, 8, nullptr, 8) == 22 && result == failed && wide[0] == 0, "mbstowcs_s accepted a null source"); + RequireViolation(before, "mbstowcs_s: bad argument", 22); + + unsigned char state[128]{}; + const char* source = "world"; + Require(mbsrtowcs_s_nid_postfix(&result, nullptr, 0, &source, 0, state) == 0 && result == 5 && std::strcmp(source, "world") == 0, + "mbsrtowcs_s did not count the characters"); + Require(mbsrtowcs_s_nid_postfix(&result, wide, 8, &source, 8, state) == 0 && result == 5 && wide[4] == 'd' && wide[5] == 0 && source == nullptr, + "mbsrtowcs_s did not convert the string"); + const char* text = "world"; + source = text; + Require(mbsrtowcs_s_nid_postfix(&result, wide, 8, &source, 2, state) == 0 && result == 2 && wide[2] == 0 && source == text + 2, + "mbsrtowcs_s did not stop at len"); + before = constraintCalls; + source = text; + Require(mbsrtowcs_s_nid_postfix(&result, wide, 3, &source, 8, state) == 34 && result == failed && wide[0] == 0 && source == text + 3, + "mbsrtowcs_s overflowed the buffer"); + RequireViolation(before, "mbsrtowcs_s: buffer too small", 34); + before = constraintCalls; + source = nullptr; + Require(mbsrtowcs_s_nid_postfix(&result, wide, 8, &source, 8, state) == 22 && result == failed, "mbsrtowcs_s accepted a null source string"); + RequireViolation(before, "mbsrtowcs_s: bad argument", 22); + before = constraintCalls; + source = text; + Require(mbsrtowcs_s_nid_postfix(&result, wide, tooLarge, &source, 8, state) == 22 && result == failed, "mbsrtowcs_s accepted an oversized buffer"); + RequireViolation(before, "mbsrtowcs_s: bad argument", 22); +} + +void CheckGets(const char* path) { + { + std::ofstream input(path, std::ios::binary | std::ios::trunc); + input << "short\nexactly8\nrest"; + Require(input.good(), "could not write the gets_s input"); + } + FileStream* const saved = __stdinp_nid_postfix; + FileStream* const stream = fopen_nid_postfix(path, "r"); + Require(stream != nullptr, "could not open the gets_s input"); + __stdinp_nid_postfix = stream; + char buffer[8]; + Require(gets_s_nid_postfix(buffer, sizeof(buffer)) == buffer && std::strcmp(buffer, "short") == 0, "gets_s did not read a line"); + int before = constraintCalls; + Require(gets_s_nid_postfix(buffer, sizeof(buffer)) == nullptr && buffer[0] == 0, "gets_s accepted a line longer than the buffer"); + RequireViolation(before, "gets_s: short buffer", 34); + Require(gets_s_nid_postfix(buffer, sizeof(buffer)) == buffer && std::strcmp(buffer, "rest") == 0, "gets_s did not discard the rest of a long line"); + buffer[0] = 'x'; + Require(gets_s_nid_postfix(buffer, sizeof(buffer)) == nullptr && buffer[0] == 0, "gets_s did not report end of file"); + before = constraintCalls; + Require(gets_s_nid_postfix(buffer, 0) == nullptr, "gets_s accepted a zero size"); + RequireViolation(before, "gets_s: zero buffer size", 22); + before = constraintCalls; + Require(gets_s_nid_postfix(nullptr, sizeof(buffer)) == nullptr, "gets_s accepted a null buffer"); + RequireViolation(before, "gets_s: bad argument", 22); + __stdinp_nid_postfix = saved; + fclose_nid_postfix(stream); + std::remove(path); +} + +} // namespace + +int main(int argc, char** argv) { + Require(argc > 1, "usage: guest_bounds_checking_tests "); + set_constraint_handler_s_nid_postfix(reinterpret_cast(RecordConstraint)); + CheckBsearch(); + CheckWcstok(); + CheckWideToMultibyte(); + CheckMultibyteToWide(); + CheckGets(argv[1]); + return 0; +} diff --git a/core/libs/tests/GuestCallTrace.cpp b/core/libs/tests/GuestCallTrace.cpp index 4be35302dc..4b7e2091e9 100644 --- a/core/libs/tests/GuestCallTrace.cpp +++ b/core/libs/tests/GuestCallTrace.cpp @@ -1,6 +1,13 @@ #include "prx/libc/include/CallTrace.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#ifdef _WIN32 #include +#else +#include +#include +#endif +#include +#include #include #include #include @@ -49,18 +56,65 @@ static Block APS5_VABI Blocks(std::uint64_t seed) { return {{seed, seed + 1, seed + 2, seed + 3}}; } +static std::uint64_t CurrentThread() { +#ifdef _WIN32 + return GetCurrentThreadId(); +#else + return static_cast(gettid()); +#endif +} + +static const void* ImageOf(const void* address) { +#ifdef _WIN32 + MEMORY_BASIC_INFORMATION memory{}; + if (VirtualQuery(address, &memory, sizeof(memory)) != sizeof(memory)) return nullptr; + return memory.AllocationBase; +#else + Dl_info info{}; + if (dladdr(address, &info) == 0) return nullptr; + return info.dli_fbase; +#endif +} + template static TFunction* Traced(TFunction* function, const char* name, const char* library) { return reinterpret_cast(TraceImport_nid_no_patch(reinterpret_cast(function), name, library)); } int main(int argc, char** argv) { +#ifdef _WIN32 Require(_putenv_s("APS5_TRACE_CALLS", "libTraced,libOther.prx") == 0); - if (argc > 1 && std::strcmp(argv[1], "abort") == 0) { +#else + Require(setenv("APS5_TRACE_CALLS", "libTraced,libOther.prx", 1) == 0); +#endif + if (argc > 1 && (std::strcmp(argv[1], "abort") == 0 || std::strcmp(argv[1], "abort-race") == 0)) { +#ifndef _WIN32 + Require(std::signal(SIGABRT, [](int) { _exit(3); }) != SIG_ERR); +#endif const auto first = Traced(&Integers, "Integers", "libTraced.prx"); const auto second = Traced(&Blocks, "Blocks", "libOther.prx"); Require(first(1, 2, 3, 4, 5, 6, 7, 8) == 204 && second(7).words[3] == 10); +#ifdef _WIN32 _set_abort_behavior(0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT); +#else + std::atomic locked{false}; + std::thread([&locked] { + flockfile(stderr); + locked.store(true); + for (;;) pause(); + }).detach(); + while (!locked.load()) std::this_thread::yield(); + if (std::strcmp(argv[1], "abort-race") == 0) { + for (int call = 0; call < 100; ++call) Require(first(1, 2, 3, 4, 5, 6, 7, 8) == 204); + std::atomic go{false}; + std::thread([&go] { + while (!go.load()) { + } + std::abort(); + }).detach(); + go.store(true); + } +#endif std::abort(); } CallTraceEntry entries[16]; @@ -90,15 +144,13 @@ int main(int argc, char** argv) { const char* const names[] = {"Integers", "Doubles", "Mixed", "Variadic", "Blocks"}; const char* const libraries[] = {"libTraced.prx", "libTraced", "libOther.prx", "libOther", "libTraced.prx"}; - MEMORY_BASIC_INFORMATION image{}; - Require(VirtualQuery(reinterpret_cast(&main), &image, sizeof(image)) == sizeof(image)); + const void* image = ImageOf(reinterpret_cast(&main)); + Require(image != nullptr); Require(CopyCallTrace_nid_no_patch(entries, 16) == 5); for (std::uint64_t index = 0; index < 5; ++index) { - Require(entries[index].sequence == index + 1 && entries[index].thread == GetCurrentThreadId()); + Require(entries[index].sequence == index + 1 && entries[index].thread == CurrentThread()); Require(std::strcmp(entries[index].name, names[index]) == 0 && std::strcmp(entries[index].library, libraries[index]) == 0); - MEMORY_BASIC_INFORMATION caller{}; - Require(VirtualQuery(reinterpret_cast(entries[index].returnAddress), &caller, sizeof(caller)) == sizeof(caller)); - Require(caller.AllocationBase == image.AllocationBase); + Require(ImageOf(reinterpret_cast(entries[index].returnAddress)) == image); } Require(CopyCallTrace_nid_no_patch(entries, 2) == 2 && entries[0].sequence == 4 && entries[1].sequence == 5); Require(CopyCallCounts_nid_no_patch(counts, 8, &total) == 5 && total == 5); @@ -109,12 +161,20 @@ int main(int argc, char** argv) { Require(CopyCallCounts_nid_no_patch(counts, 2, nullptr) == 2); Require(counts[0].calls == 3 && std::strcmp(counts[0].name, "Blocks") == 0 && counts[1].calls == 2 && std::strcmp(counts[1].name, "Doubles") == 0); Require(CopyCallCounts_nid_no_patch(nullptr, 8, &total) == 0 && total == 8); + Require(CopyThreadCallTrace_nid_no_patch(CurrentThread(), entries, 16) == 8); + for (std::uint64_t index = 0; index < 8; ++index) Require(entries[index].sequence == index + 1 && entries[index].thread == CurrentThread()); + Require(CopyThreadCallTrace_nid_no_patch(CurrentThread(), entries, 2) == 2 && entries[0].sequence == 7 && entries[1].sequence == 8); + Require(std::strcmp(entries[1].name, "Blocks") == 0); + Require(CopyThreadCallTrace_nid_no_patch(CurrentThread() + 1, entries, 16) == 0); + Require(CopyThreadCallTrace_nid_no_patch(CurrentThread(), nullptr, 16) == 0 && CopyThreadCallTrace_nid_no_patch(CurrentThread(), entries, 0) == 0); constexpr std::uint64_t Workers = 4; constexpr std::uint64_t Calls = 20000; std::vector workers; + std::uint64_t workerThreads[Workers]{}; for (std::uint64_t worker = 0; worker < Workers; ++worker) { - workers.emplace_back([integers, worker] { + workers.emplace_back([integers, worker, &workerThreads] { + workerThreads[worker] = CurrentThread(); for (std::uint64_t call = 0; call < Calls; ++call) Require(integers(worker, call, 1, 2, 3, 4, 5, 6) == Integers(worker, call, 1, 2, 3, 4, 5, 6)); }); } @@ -124,6 +184,19 @@ int main(int argc, char** argv) { Require(CopyCallTrace_nid_no_patch(recent.get(), Window) == Window); Require(recent[Window - 1].sequence == 8 + Workers * Calls && recent[0].sequence == 8 + Workers * Calls - Window + 1); for (std::size_t index = 0; index < Window; ++index) Require(std::strcmp(recent[index].name, "Integers") == 0); + Require(CopyThreadCallTrace_nid_no_patch(CurrentThread(), entries, 16) == 0); + const auto own = std::make_unique(Window); + std::size_t owned = 0; + for (const std::uint64_t thread : workerThreads) { + std::size_t expected = 0; + for (std::size_t index = 0; index < Window; ++index) expected += recent[index].thread == thread; + const std::size_t count = CopyThreadCallTrace_nid_no_patch(thread, own.get(), Window); + Require(count == expected); + for (std::size_t index = 0; index < count; ++index) Require(own[index].thread == thread && (index == 0 || own[index - 1].sequence < own[index].sequence)); + if (count != 0) Require(CopyThreadCallTrace_nid_no_patch(thread, entries, 1) == 1 && entries[0].sequence == own[count - 1].sequence); + owned += count; + } + Require(owned == Window); Require(CopyCallCounts_nid_no_patch(counts, 1, &total) == 1 && total == 8 + Workers * Calls); Require(counts[0].calls == 1 + Workers * Calls && std::strcmp(counts[0].name, "Integers") == 0); } diff --git a/core/libs/tests/GuestCes.cpp b/core/libs/tests/GuestCes.cpp index 840b454b6f..7e5238dce7 100644 --- a/core/libs/tests/GuestCes.cpp +++ b/core/libs/tests/GuestCes.cpp @@ -160,6 +160,7 @@ int main() { std::uint32_t used = 0; std::uint32_t produced = 0; char sheet[1024] = {}; + Require(sceCesUcsProfileInitEucJpCp51932(nullptr) == nullptr); void* eucJp = sceCesUcsProfileInitEucJpCp51932(sheet); Require(eucJp != nullptr); char eucContext[256] = {}; @@ -172,6 +173,8 @@ int main() { Require(sceCesUtf8StrToMbcsStr(eucContext, reinterpret_cast("\xF0\x9F\x98\x80"), 4, &used, mbcs, sizeof(mbcs), &produced) == unassignedCode); std::uint8_t back[16] = {}; + Require(sceCesMbcsStrToUtf8Str(nullptr, mbcs, 6, &used, back, sizeof(back), &produced) == invalidParameter); + Require(sceCesMbcsStrToUtf8Str(eucContext, mbcs, 6, &used, nullptr, sizeof(back), &produced) == invalidDstBuffer); Require(sceCesMbcsStrToUtf8Str(eucContext, mbcs, 6, &used, back, sizeof(back), &produced) == 0); Require(used == 6 && produced == 7 && std::memcmp(back, eucSource, 7) == 0); Require(sceCesMbcsStrGetUtf8Len(eucContext, mbcs, 6, &used, &produced) == 0 && produced == 7); @@ -257,6 +260,66 @@ int main() { } Require(supplementaryAssigned == 6067); + const auto* sjisSource = reinterpret_cast("Go \xEF\xBD\xB1!"); + Require(sceCesUtf8StrToMbcsStr(sjisContext, sjisSource, 16, &used, mbcs, sizeof(mbcs), &produced) == 0); + Require(used == 7 && produced == 5 && std::memcmp(mbcs, "Go \xB1!", 5) == 0); + Require(sceCesUtf8StrGetMbcsLen(sjisContext, sjisSource, 16, &used, &produced) == 0 && used == 7 && produced == 5); + Require(sceCesUtf8StrToMbcsStr(sjisContext, sjisSource, 16, &used, mbcs, 4, &produced) == dstBufferEnd); + Require(sceCesUtf8StrToMbcsStr(sjisContext, reinterpret_cast("\xF0\x9F\x98\x80"), 4, &used, mbcs, sizeof(mbcs), &produced) == unassignedCode); + + std::uint8_t sjisBack[16] = {}; + Require(sceCesMbcsStrToUtf8Str(sjisContext, mbcs, 5, &used, sjisBack, sizeof(sjisBack), &produced) == 0); + Require(used == 5 && produced == 7 && std::memcmp(sjisBack, sjisSource, 7) == 0); + Require(sceCesMbcsStrGetUtf8Len(sjisContext, mbcs, 5, &used, &produced) == 0 && produced == 7); + Require(sceCesMbcsStrToUtf8Str(sjisContext, reinterpret_cast("\x82"), 1, &used, sjisBack, sizeof(sjisBack), &produced) == srcBufferEnd); + Require(sceCesMbcsStrToUtf8Str(sjisContext, reinterpret_cast("\x82\x20"), 2, &used, sjisBack, sizeof(sjisBack), &produced) == invalidEncode); + Require(sceCesMbcsStrToUtf8Str(sjisContext, reinterpret_cast("\x82\x7F"), 2, &used, sjisBack, sizeof(sjisBack), &produced) == invalidEncode); + + const std::uint16_t sjisUcs2[] = {u'O', u'K', 0xFF9F, 0}; + Require(sceCesUcs2StrToMbcsStr(sjisContext, sjisUcs2, 4, &used, mbcs, sizeof(mbcs), &produced) == 0); + Require(used == 3 && produced == 3 && std::memcmp(mbcs, "OK\xDF", 3) == 0); + Require(sceCesUcs2StrGetMbcsLen(sjisContext, sjisUcs2, 4, &used, &produced) == 0 && produced == 3); + std::uint16_t sjisWide[8] = {}; + Require(sceCesMbcsStrToUcs2Str(sjisContext, mbcs, 3, &used, sjisWide, 8, &produced) == 0); + Require(used == 3 && produced == 3 && sjisWide[0] == u'O' && sjisWide[1] == u'K' && sjisWide[2] == 0xFF9F); + Require(sceCesMbcsStrGetUcs2Len(sjisContext, mbcs, 3, &used, &produced) == 0 && produced == 3); + + Require(sceCesUtf8StrToMbcsStr(sjisContext, japanese, 24, &used, mbcs, sizeof(mbcs), &produced) == 0); + Require(used == 24 && produced == 16 && std::memcmp(mbcs, "\x82\xB1\x82\xF1\x82\xC9\x82\xBF\x82\xCD\x93\xFA\x96\x7B\x8C\xEA", 16) == 0); + std::uint8_t sjisJapaneseBack[32] = {}; + Require(sceCesMbcsStrToUtf8Str(sjisContext, mbcs, 16, &used, sjisJapaneseBack, sizeof(sjisJapaneseBack), &produced) == 0 && produced == 24 && std::memcmp(sjisJapaneseBack, japanese, 24) == 0); + Require(sceCesMbcsStrToUcs2Str(sjisContext, mbcs, 16, &used, sjisWide, 8, &produced) == 0 && used == 16 && produced == 8 && sjisWide[0] == 0x3053 && sjisWide[5] == 0x65E5 && sjisWide[7] == 0x8A9E); + Require(sceCesMbcsStrToUcs2Str(sjisContext, mbcs, 15, &used, sjisWide, 8, &produced) == srcBufferEnd); + const std::uint16_t sjisExtensions[] = {0x2460, 0xFF5E, 0x2252, 0}; + Require(sceCesUcs2StrToMbcsStr(sjisContext, sjisExtensions, 4, &used, mbcs, sizeof(mbcs), &produced) == 0 && produced == 6 && std::memcmp(mbcs, "\x87\x40\x81\x60\x81\xE0", 6) == 0); + Require(sceCesMbcsStrToUcs2Str(sjisContext, reinterpret_cast("\x85\x40"), 2, &used, sjisWide, 8, &produced) == unassignedCode); + Require(sceCesMbcsStrToUcs2Str(sjisContext, reinterpret_cast("\x80"), 1, &used, sjisWide, 8, &produced) == invalidEncode); + Require(sceCesMbcsStrToUcs2Str(sjisContext, reinterpret_cast("\xA0"), 1, &used, sjisWide, 8, &produced) == invalidEncode); + Require(sceCesUcs2StrToMbcsStr(sjisContext, yen, 2, &used, mbcs, sizeof(mbcs), &produced) == unassignedCode); + Require(sceCesMbcsStrToUcs2Str(sjisContext, reinterpret_cast("\xFB\xFC"), 2, &used, sjisWide, 8, &produced) == 0 && used == 2 && produced == 1 && sjisWide[0] == 0x9AD9); + Require(sceCesMbcsStrToUcs2Str(sjisContext, reinterpret_cast("\xFA\x40"), 2, &used, sjisWide, 8, &produced) == 0 && used == 2 && produced == 1 && sjisWide[0] == 0x2170); + Require(sceCesMbcsStrToUcs2Str(sjisContext, reinterpret_cast("\xFC\x4B"), 2, &used, sjisWide, 8, &produced) == 0 && used == 2 && produced == 1 && sjisWide[0] == 0x9ED1); + Require(sceCesMbcsStrToUcs2Str(sjisContext, reinterpret_cast("\xFC\x4C"), 2, &used, sjisWide, 8, &produced) == unassignedCode); + Require(sceCesMbcsStrToUcs2Str(sjisContext, reinterpret_cast("\xF0\x40"), 2, &used, sjisWide, 8, &produced) == unassignedCode); + std::uint32_t sjisAssigned = 0; + for (std::uint32_t lead = 0x81; lead <= 0xFC; ++lead) { + if (lead > 0x9F && lead < 0xE0) continue; + for (std::uint32_t trail = 0x40; trail <= 0xFC; ++trail) { + if (trail == 0x7F) continue; + const std::uint8_t cell[2] = {static_cast(lead), static_cast(trail)}; + std::uint16_t decoded[2] = {}; + if (sceCesMbcsStrToUcs2Str(sjisContext, cell, 2, &used, decoded, 2, &produced) != 0) continue; + ++sjisAssigned; + decoded[1] = 0; + std::uint8_t again[4] = {}; + std::uint16_t second[2] = {}; + Require(sceCesUcs2StrToMbcsStr(sjisContext, decoded, 2, &used, again, sizeof(again), &produced) == 0 && produced == 2); + Require(sceCesMbcsStrToUcs2Str(sjisContext, again, 2, &used, second, 2, &produced) == 0 && second[0] == decoded[0]); + if (again[0] != cell[0] || again[1] != cell[1]) Require(again[0] < cell[0] || (again[0] == cell[0] && again[1] < cell[1])); + } + } + Require(sjisAssigned == 7724); + Require(sceCesMbcsStrToUtf8Str(context, reinterpret_cast("\xC0\xFF"), 2, &used, back, sizeof(back), &produced) == 0); Require(produced == 4 && std::memcmp(back, "\xD0\x90\xD1\x8F", 4) == 0); Require(sceCesUtf8StrToMbcsStr(context, back, 4, &used, mbcs, sizeof(mbcs), &produced) == 0 && produced == 2 && mbcs[0] == 0xC0 && mbcs[1] == 0xFF); 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/GuestContentDelete.cpp b/core/libs/tests/GuestContentDelete.cpp index 8a79bb03db..9353f3a578 100644 --- a/core/libs/tests/GuestContentDelete.cpp +++ b/core/libs/tests/GuestContentDelete.cpp @@ -30,6 +30,8 @@ bool Contains(const std::string& text, const std::string& part) { return text.fi } int main() { + Require(LogicErrorMessage([] { sceContentDeleteTerminate(); }) == "sceContentDeleteTerminate: not initialized"); + Require(LogicErrorMessage([] { sceContentDeleteInitialize(nullptr); }) == "invalid argument"); Require(LogicErrorMessage([] { sceContentDeleteById(1); }) == "sceContentDeleteById: not initialized"); Require(LogicErrorMessage([] { sceContentDeleteByPath("/user/av_contents/photo/a.png"); }) == "sceContentDeleteByPath: not initialized"); Require(LogicErrorMessage([] { sceContentDeleteByPath(nullptr); }) == "invalid argument"); @@ -37,6 +39,7 @@ int main() { ContentDeleteInitParam param{}; param.heap_size = 0x10000; Require(sceContentDeleteInitialize(¶m) == 0); + Require(LogicErrorMessage([&] { sceContentDeleteInitialize(¶m); }) == "sceContentDeleteInitialize: already initialized"); Require(LogicErrorMessage([] { sceContentDeleteByPath(nullptr); }) == "invalid argument"); Require(Contains(LogicErrorMessage([] { sceContentDeleteByPath("/user/av_contents/photo/a.png"); }), "sceContentDeleteByPath: /user/av_contents/photo/a.png is not in the host content library")); @@ -44,6 +47,7 @@ int main() { Require(Contains(LogicErrorMessage([] { sceContentDeleteById(-1); }), "sceContentDeleteById: content id -1 is not in the host content library")); Require(sceContentDeleteTerminate() == 0); + Require(LogicErrorMessage([] { sceContentDeleteTerminate(); }) == "sceContentDeleteTerminate: not initialized"); Require(LogicErrorMessage([] { sceContentDeleteById(1); }) == "sceContentDeleteById: not initialized"); return 0; } 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/GuestConvertKeycode.cpp b/core/libs/tests/GuestConvertKeycode.cpp index 7e4b8c1cc4..0e7095e097 100644 --- a/core/libs/tests/GuestConvertKeycode.cpp +++ b/core/libs/tests/GuestConvertKeycode.cpp @@ -28,6 +28,8 @@ int main() { type = 0xFFFFFFFFu; Require(sceConvertKeycodeGetImeKeyboardType(1, &type) == 0); Require(type == 0); + + Require(sceConvertKeycodeGetCharacter(0x04, 0, 0, nullptr) == invalidAddress); for (std::uint32_t state : {0u, 0x4000000u}) { char16_t character[3] = {u'?', 0, 0}; Require(sceConvertKeycodeGetCharacter(0x04, state, 0, character) == 0); @@ -40,6 +42,18 @@ int main() { Require(character[0] == u'\\'); Require(sceConvertKeycodeGetCharacter(0x32, state, 0, character) == 0); Require(character[0] == 0); + Require(sceConvertKeycodeGetCharacter(0x27, state, 0, character) == 0); + Require(character[0] == u'0'); + Require(sceConvertKeycodeGetCharacter(0x2C, state, 0, character) == 0); + Require(character[0] == u' '); + Require(sceConvertKeycodeGetCharacter(0x2D, state, 0, character) == 0); + Require(character[0] == u'-'); + Require(sceConvertKeycodeGetCharacter(0x54, state, 0, character) == 0); + Require(character[0] == u'/'); + Require(sceConvertKeycodeGetCharacter(0x59, state, 0, character) == 0); + Require(character[0] == u'1'); + Require(sceConvertKeycodeGetCharacter(0x63, state, 0, character) == 0); + Require(character[0] == u'.'); } return 0; } 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/GuestDatagramReceive.cpp b/core/libs/tests/GuestDatagramReceive.cpp index 22cf4caaf8..2c7bc9e0c5 100644 --- a/core/libs/tests/GuestDatagramReceive.cpp +++ b/core/libs/tests/GuestDatagramReceive.cpp @@ -40,8 +40,12 @@ static void Require(bool condition, const char* message) { } static void Check(bool net, bool from, int family) { + int* error = net ? sceNetErrnoLoc() : __error_nid_postfix(); const auto create = [&] { return net ? sceNetSocket(nullptr, family, 2, 17) : socket_nid_postfix(family, 2, 17); }; - const int receiver = create(), sender = create(); + const auto closeSocket = [&](int descriptor) { return net ? sceNetSocketClose(descriptor) : close_nid_postfix(descriptor); }; + const int receiver = create(); + if (receiver < 0 && family == 28 && *error == 47) return; + const int sender = create(); Require(receiver >= 0 && sender >= 0, "create UDP sockets"); std::array destination{}; const std::uint32_t addressSize = family == 2 ? 16 : 28; @@ -53,7 +57,12 @@ static void Check(bool net, bool from, int family) { return net ? sceNetBind_nid_postfix(descriptor, destination.data(), addressSize) : bind_nid_postfix(descriptor, destination.data(), addressSize); }; - Require(bind(receiver) == 0 && bind(sender) == 0, "bind loopback sockets"); + const int bound = bind(receiver); + if (bound < 0 && family == 28 && *error == 49) { + Require(closeSocket(sender) == 0 && closeSocket(receiver) == 0, "close sockets without IPv6 loopback"); + return; + } + Require(bound == 0 && bind(sender) == 0, "bind loopback sockets"); auto senderAddress = destination; std::uint32_t size = addressSize; Require((net ? sceNetGetsockname(sender, senderAddress.data(), &size) : @@ -66,7 +75,6 @@ static void Check(bool net, bool from, int family) { const int enabled = 1; Require((net ? sceNetSetsockopt(receiver, 0xffff, 0x1200, &enabled, sizeof(enabled)) : fcntl_nid_postfix(receiver, 4, 6)) == 0, "make receiver nonblocking"); - int* error = net ? sceNetErrnoLoc() : __error_nid_postfix(); const auto receive = [&](void* buffer, std::size_t length, int flags, void* address, std::uint32_t* addressLength) { if (net) return from ? sceNetRecvfrom(receiver, buffer, length, flags, address, addressLength) : sceNetRecv(receiver, buffer, length, flags); @@ -132,8 +140,8 @@ static void Check(bool net, bool from, int family) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); } while (std::chrono::steady_clock::now() < deadline); Require(result == 0 && byte == '!', "empty datagram"); - Require((net ? sceNetSocketClose(receiver) : close_nid_postfix(receiver)) == 0, "close receiver"); - Require((net ? sceNetSocketClose(sender) : close_nid_postfix(sender)) == 0, "close sender"); + Require(closeSocket(receiver) == 0, "close receiver"); + Require(closeSocket(sender) == 0, "close sender"); } int main(int argc, char** argv) { diff --git a/core/libs/tests/GuestDirectory.cpp b/core/libs/tests/GuestDirectory.cpp index 5c3e5dd0cc..4df4394622 100644 --- a/core/libs/tests/GuestDirectory.cpp +++ b/core/libs/tests/GuestDirectory.cpp @@ -1,7 +1,9 @@ #include "prx/libc/include/GuestDirectory.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include "SceTypes.hpp" #include #include +#include #include #include #include @@ -9,6 +11,10 @@ #include #include #include +#include +#ifdef _WIN32 +#include +#endif extern "C" { void* APS5_VABI opendir_nid_postfix(const char*); GuestDirectoryEntry* APS5_VABI readdir_nid_postfix(void*); @@ -18,25 +24,83 @@ void APS5_VABI rewinddir_nid_postfix(void*); std::int64_t APS5_VABI telldir_nid_postfix(void*); void APS5_VABI seekdir_nid_postfix(void*, std::int64_t); int* APS5_VABI __error_nid_postfix(); +int APS5_VABI lstat_nid_postfix(const char*, FileStat*); } static void Require(bool value) { if (!value) std::abort(); } +static std::map ReadFileNumbers(void* directory) { + std::map numbers; + rewinddir_nid_postfix(directory); + while (auto* entry = readdir_nid_postfix(directory)) { + Require(entry->fileNumber != 0); + numbers[entry->name] = entry->fileNumber; + } + return numbers; +} +static std::uint32_t FileNumber(const std::filesystem::path& path) { + FileStat status{}; + Require(lstat_nid_postfix(path.string().c_str(), &status) == 0); + return static_cast(status.st_ino); +} +static bool CreateLinks(const std::filesystem::path& root) { +#ifdef _WIN32 + if (CreateSymbolicLinkW((root / "file-link").c_str(), L"sample.txt", SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) == 0) { + Require(GetLastError() == ERROR_PRIVILEGE_NOT_HELD); + std::fprintf(stderr, "Windows readdir link checks skipped: creating symbolic links needs Developer Mode or administrator rights\n"); + return false; + } + Require(CreateSymbolicLinkW((root / "directory-link").c_str(), L"subdirectory", + SYMBOLIC_LINK_FLAG_DIRECTORY | SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0); +#else + std::filesystem::create_symlink("sample.txt", root / "file-link"); + std::filesystem::create_directory_symlink("subdirectory", root / "directory-link"); +#endif + return true; +} int main() { const auto root = std::filesystem::path("anyps5-directory-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); Require(std::filesystem::create_directory(root)); Require(std::filesystem::create_directory(root / "subdirectory")); { std::ofstream file(root / "sample.txt"); file << "test"; } + const bool links = CreateLinks(root); + std::vector numbered{"sample.txt", "subdirectory"}; + if (links) numbered.insert(numbered.end(), {"file-link", "directory-link"}); void* directory = opendir_nid_postfix(root.string().c_str()); Require(directory != nullptr); std::map entries; + std::map fileNumbers; *__error_nid_postfix() = 13; while (auto* entry = readdir_nid_postfix(directory)) { Require(entry->nameLength == std::strlen(entry->name)); Require(entry->recordLength == 8 + ((entry->nameLength + 4) & ~3)); Require(entry->recordLength <= sizeof(*entry)); entries[entry->name] = entry->type; + Require(entry->fileNumber != 0); + fileNumbers[entry->name] = entry->fileNumber; } Require(*__error_nid_postfix() == 13); // EOF preserves errno. + for (const char* name : numbered) Require(fileNumbers.at(name) == FileNumber(root / name)); + if (links) Require(fileNumbers.at("file-link") != fileNumbers.at("sample.txt")); + { std::ofstream file(root / "created.txt"); file << "new"; } + auto changed = ReadFileNumbers(directory); + Require(changed.at("created.txt") == FileNumber(root / "created.txt")); + Require(changed.at("sample.txt") == fileNumbers.at("sample.txt")); + Require(std::filesystem::remove(root / "created.txt")); + changed = ReadFileNumbers(directory); + Require(changed.count("created.txt") == 0 && changed == fileNumbers); +#ifdef _WIN32 + const HANDLE pending = CreateFileW((root / "pending.txt").c_str(), GENERIC_WRITE | DELETE, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, CREATE_NEW, 0, nullptr); + Require(pending != INVALID_HANDLE_VALUE); + FILE_DISPOSITION_INFO disposition{TRUE}; + Require(SetFileInformationByHandle(pending, FileDispositionInfo, &disposition, sizeof(disposition)) != 0); + FileStat pendingStatus{}; + Require(lstat_nid_postfix((root / "pending.txt").string().c_str(), &pendingStatus) == -1); + changed = ReadFileNumbers(directory); + Require(changed.count("pending.txt") == 1 && changed.at("sample.txt") == fileNumbers.at("sample.txt")); + Require(CloseHandle(pending) != 0); + Require(ReadFileNumbers(directory) == fileNumbers); +#endif Require(entries.at("sample.txt") == 8); Require(entries.at("subdirectory") == 4); Require(entries.at(".") == 4 && entries.at("..") == 4); @@ -99,6 +163,7 @@ int main() { seekdir_nid_postfix(directory, second); auto* entryAfterSeek = readdir_nid_postfix(directory); Require(entryAfterSeek != nullptr && secondName == entryAfterSeek->name); + Require(entryAfterSeek->fileNumber == fileNumbers.at(secondName)); *__error_nid_postfix() = 0; Require(telldir_nid_postfix(nullptr) == -1 && *__error_nid_postfix() == 9); *__error_nid_postfix() = 0; @@ -133,6 +198,10 @@ int main() { Require(names.at(std::string(file.begin(), file.end())) == 8 && names.size() == 3); Require(closedir_nid_postfix(unicode) == 0); std::filesystem::remove_all(root / std::filesystem::path(folder)); + if (links) { + std::filesystem::remove(root / "file-link"); + std::filesystem::remove(root / "directory-link"); + } std::filesystem::remove(root / "sample.txt"); std::filesystem::remove(root / "subdirectory"); std::filesystem::remove(root); diff --git a/core/libs/tests/GuestDirectoryBuffer.cpp b/core/libs/tests/GuestDirectoryBuffer.cpp index 230708a2f4..e5d0c6c122 100644 --- a/core/libs/tests/GuestDirectoryBuffer.cpp +++ b/core/libs/tests/GuestDirectoryBuffer.cpp @@ -102,7 +102,7 @@ static void CheckReadOnlyDestination() { ("anyps5-directory-readonly-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); Require(std::filesystem::create_directory(root)); { std::ofstream file(root / "entry.bin"); Require(static_cast(file)); } - const int directory = sceKernelOpen(root.string().c_str(), SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); + int directory = sceKernelOpen(root.string().c_str(), SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); Require(directory >= 0); std::int64_t physical = 0; Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page, 0, 0, &physical) == 0); @@ -110,6 +110,9 @@ static void CheckReadOnlyDestination() { Require(sceKernelMapDirectMemory(&mapping, page, 3, 0, physical, 0) == 0 && mapping != nullptr); std::int64_t base = -1; Require(sceKernelGetdirentries(directory, static_cast(mapping), 4096, &base) > 0); + Require(sceKernelClose(directory) == 0); + directory = sceKernelOpen(root.string().c_str(), SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); + Require(directory >= 0); Require(sceKernelMprotect(mapping, page, 1) == 0); Require(sceKernelGetdirentries(directory, static_cast(mapping), 4096, &base) == SCE_KERNEL_ERROR_EFAULT); Require(sceKernelGetdirentries(12345, static_cast(mapping), 4096, &base) == SCE_KERNEL_ERROR_EBADF); @@ -117,6 +120,7 @@ static void CheckReadOnlyDestination() { Require(sceKernelGetdirentries(directory, buffer.data(), static_cast(buffer.size()), static_cast(mapping)) == SCE_KERNEL_ERROR_EFAULT); Require(sceKernelMprotect(mapping, page, 3) == 0); + Require(sceKernelGetdirentries(directory, buffer.data(), static_cast(buffer.size()), &base) > 0); Require(sceKernelMunmap(mapping, page) == 0); Require(sceKernelReleaseDirectMemory(physical, page) == 0); Require(sceKernelClose(directory) == 0); diff --git a/core/libs/tests/GuestDynamicLoader.cpp b/core/libs/tests/GuestDynamicLoader.cpp index 3a83d086a9..8ea9fe7c93 100644 --- a/core/libs/tests/GuestDynamicLoader.cpp +++ b/core/libs/tests/GuestDynamicLoader.cpp @@ -19,6 +19,8 @@ int APS5_VABI dlclose_nid_postfix(void*); char* APS5_VABI dlerror_nid_postfix(); int APS5_VABI _sceKernelRtldThreadAtexitIncrement_nid_postfix(const void*); int APS5_VABI _sceKernelRtldThreadAtexitDecrement_nid_postfix(const void*); +int APS5_VABI sceKernelLoadStartModule(const char*, std::size_t, const void*, unsigned, const void*, int*); +int APS5_VABI sceKernelStopUnloadModule(int, std::size_t, const void*, unsigned, const void*, int*); } static void Require(bool value, const std::source_location& location = std::source_location::current()) { if (value) return; @@ -166,6 +168,25 @@ int main(int argc, char** argv) { std::filesystem::remove(guest); std::filesystem::remove(relinked); + const bool createdLibs = std::filesystem::create_directories("libs"); + std::filesystem::copy_file(argv[1], "libs/anyps5-system-module-for-test.prx", std::filesystem::copy_options::overwrite_existing); + for (const char* path : {"/gkbHtXc3Wq/common/lib/anyps5-system-module-for-test.sprx", "/system/common/lib/anyps5-system-module-for-test.sprx"}) { + int started = -1; + const int system = sceKernelLoadStartModule(path, 0, nullptr, 0, nullptr, &started); + Require(system > 0 && started == 0); + add = reinterpret_cast(dlsym_nid_postfix(reinterpret_cast(static_cast(system)), "GuestModuleAdd")); + Require(add && add(30, 12) == 42); + Require(sceKernelStopUnloadModule(system, 0, nullptr, 0, nullptr, nullptr) == 0); + } + Require(sceKernelLoadStartModule("/gkbHtXc3Wq/common/lib/anyps5-missing-system-module.sprx", 0, nullptr, 0, nullptr, nullptr) == static_cast(0x80020002u)); + Require(sceKernelLoadStartModule("/gkbHtXc3Wq/common/lib/anyps5-system-module-for-test.prx", 0, nullptr, 0, nullptr, nullptr) == static_cast(0x80020002u)); + std::filesystem::copy_file(argv[1], "anyps5-escape-module-for-test.prx", std::filesystem::copy_options::overwrite_existing); + Require(sceKernelLoadStartModule("/gkbHtXc3Wq/common/lib/../anyps5-escape-module-for-test.sprx", 0, nullptr, 0, nullptr, nullptr) == static_cast(0x80020002u)); + std::filesystem::remove("anyps5-escape-module-for-test.prx"); + std::filesystem::remove("libs/anyps5-system-module-for-test.prx"); + std::error_code notEmpty; + if (createdLibs) std::filesystem::remove("libs", notEmpty); + void* pinned = dlopen_nid_postfix(argv[1], 2); add = reinterpret_cast(dlsym_nid_postfix(pinned, "GuestModuleAdd")); Require(add && _sceKernelRtldThreadAtexitIncrement_nid_postfix(reinterpret_cast(add)) == 0); diff --git a/core/libs/tests/GuestDynamicLoaderVariants.cpp b/core/libs/tests/GuestDynamicLoaderVariants.cpp new file mode 100644 index 0000000000..5dd0f764b8 --- /dev/null +++ b/core/libs/tests/GuestDynamicLoaderVariants.cpp @@ -0,0 +1,24 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +extern "C" { +void* APS5_VABI dlopen_nid_postfix(const char*, int); +void* APS5_VABI dlsym_nid_postfix(void*, const char*); +int APS5_VABI dlclose_nid_postfix(void*); +} +static void Require(bool value, const std::source_location& location = std::source_location::current()) { + if (value) return; + std::fprintf(stderr, "guest_dynamic_loader_variants: failed at %s:%u\n", location.file_name(), location.line()); + std::abort(); +} +int main() { + using Add = int (APS5_VABI *)(int, int); + void* extension = dlopen_nid_postfix("guest_module_fixture.prx", 2); + Require(extension != nullptr); + auto add = reinterpret_cast(dlsym_nid_postfix(extension, "GuestModuleAdd")); + Require(add && add(19, 23) == 42); + Require(dlclose_nid_postfix(extension) == 0); + Require(dlopen_nid_postfix("guest_module_fixture.other.prx", 2) == nullptr); + return 0; +} diff --git a/core/libs/tests/GuestElfPhdrMatch.cpp b/core/libs/tests/GuestElfPhdrMatch.cpp index 5e021769a4..3e8d585a70 100644 --- a/core/libs/tests/GuestElfPhdrMatch.cpp +++ b/core/libs/tests/GuestElfPhdrMatch.cpp @@ -1,5 +1,6 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/specifics/linux/ElfTypes.hpp" +#include "SceTypes.hpp" #include #include #include @@ -41,4 +42,17 @@ int main() { info.dlpi_phnum = 0; Require(__elf_phdr_match_addr_nid_postfix(&info, Address(0x403800)) == 0); + + ModuleInfoEx module{}; + module.st_size = sizeof(module); + module.segment_count = 3; + module.segments[0] = {0x401000, 0x1000, 1 | 2}; + module.segments[1] = {0x403000, 0x1000, 1 | 4}; + module.segments[2] = {0x408000, 0x100, 1 | 2 | 4}; + + Require(__elf_phdr_match_addr_nid_postfix(reinterpret_cast(&module), Address(0x403000)) == 1); + Require(__elf_phdr_match_addr_nid_postfix(reinterpret_cast(&module), Address(0x403ff8)) == 1); + Require(__elf_phdr_match_addr_nid_postfix(reinterpret_cast(&module), Address(0x404000)) == 0); + Require(__elf_phdr_match_addr_nid_postfix(reinterpret_cast(&module), Address(0x401800)) == 0); + Require(__elf_phdr_match_addr_nid_postfix(reinterpret_cast(&module), Address(0x408010)) == 1); } diff --git a/core/libs/tests/GuestErr.cpp b/core/libs/tests/GuestErr.cpp index 942936c8f7..cf7f656594 100644 --- a/core/libs/tests/GuestErr.cpp +++ b/core/libs/tests/GuestErr.cpp @@ -172,7 +172,7 @@ int main() { Require(capture.Take() == "prog: 7-x: " + denied + "\n", "vwarnc uses its code"); CallVariant(Variant::Warnc, 0, 13, nullptr); Require(capture.Take() == "prog: " + denied + "\n", "vwarnc without format"); - CallVariant(Variant::Warnx, 0, 0, "%d %d %d %d %d %d %d %d %.2f %s %ld", 1, 2, 3, 4, 5, 6, 7, 8, 2.5, "s", 123456789L); + CallVariant(Variant::Warnx, 0, 0, "%d %d %d %d %d %d %d %d %.2f %s %ld", 1, 2, 3, 4, 5, 6, 7, 8, 2.5, "s", 123456789LL); Require(capture.Take() == "prog: 1 2 3 4 5 6 7 8 2.50 s 123456789\n", "vwarnx stack and floating arguments"); CallVariant(Variant::WarnxSkipping, 0, 0, "after %d", 1, 2); Require(capture.Take() == "prog: after 2\n", "vwarnx continues an advanced list"); diff --git a/core/libs/tests/GuestExecuteOnceConcurrency.cpp b/core/libs/tests/GuestExecuteOnceConcurrency.cpp new file mode 100644 index 0000000000..e5fc8f802e --- /dev/null +++ b/core/libs/tests/GuestExecuteOnceConcurrency.cpp @@ -0,0 +1,124 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +using Callback = int (APS5_VABI *)(void*, void*, void**); +using ExecuteFunction = int (APS5_VABI *)(int*, Callback, void*); +extern "C" { +int APS5_VABI _ZSt13_Execute_onceRSt9once_flagPFiPvS1_PS1_ES1__nid_postfix(int*, Callback, void*); +int APS5_VABI std_execute_once_nid_postfix(int*, Callback, void*); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "Execute-once concurrency check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static ExecuteFunction execute = nullptr; + +struct IndependentState { + int outer = 0; + int inner = 0; + int published = 0; + int calls = 0; +}; + +static int APS5_VABI InitializeInner(void*, void* argument, void**) { + auto& state = *static_cast(argument); + ++state.calls; + state.published = 42; + return 1; +} + +static int APS5_VABI InitializeOuter(void*, void* argument, void**) { + auto& state = *static_cast(argument); + std::thread worker([&] { + Require(execute(&state.inner, InitializeInner, &state) == 1); + }); + worker.join(); + Require(state.published == 42); + return 1; +} + +static void IndependentFlags() { + IndependentState state; + std::promise completed; + auto ready = completed.get_future(); + std::thread worker([&] { + Require(execute(&state.outer, InitializeOuter, &state) == 1); + completed.set_value(); + }); + Require(ready.wait_for(std::chrono::seconds(5)) == std::future_status::ready); + worker.join(); + Require(state.outer == 1 && state.inner == 1 && state.calls == 1 && state.published == 42); +} + +struct RetryState { + int flag = 0; + int published = 0; + bool throwing = false; + std::atomic arrived = 0; + std::atomic attempts = 0; + std::atomic failures = 0; + std::atomic exceptions = 0; +}; + +static constexpr unsigned Workers = 16; + +static int APS5_VABI Retry(void*, void* argument, void**) { + auto& state = *static_cast(argument); + const auto attempt = ++state.attempts; + Require(attempt <= 2); + if (attempt == 1) { + while (state.arrived.load() != Workers) std::this_thread::yield(); + if (state.throwing) throw 17; + return 0; + } + state.published = 73; + return 5; +} + +static void ConcurrentRetry(bool throwing) { + RetryState state; + state.throwing = throwing; + std::array workers; + for (auto& worker : workers) { + worker = std::thread([&] { + ++state.arrived; + try { + const auto result = execute(&state.flag, Retry, &state); + if (result == 0) ++state.failures; + else Require(result == 1 && state.published == 73); + } catch (int value) { + Require(value == 17); + ++state.exceptions; + } + }); + } + for (auto& worker : workers) worker.join(); + Require(state.flag == 1 && state.published == 73 && state.attempts == 2); + Require(state.failures == (throwing ? 0u : 1u)); + Require(state.exceptions == (throwing ? 1u : 0u)); +} + +int main(int argc, char** argv) { + Require(argc == 2); + if (std::strcmp(argv[1], "mangled") == 0) execute = _ZSt13_Execute_onceRSt9once_flagPFiPvS1_PS1_ES1__nid_postfix; + else { + Require(std::strcmp(argv[1], "plain") == 0); + execute = std_execute_once_nid_postfix; + } + IndependentFlags(); + ConcurrentRetry(false); + ConcurrentRetry(true); + IndependentFlags(); +} 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 5cd80d4496..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,16 +258,17 @@ 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{}; Require(::CreateProcessW(nullptr, command.data(), nullptr, nullptr, FALSE, CREATE_NO_WINDOW, nullptr, nullptr, &startup, &process) != 0); ::CloseHandle(process.hThread); - if (::WaitForSingleObject(process.hProcess, 3000) != WAIT_OBJECT_0) { + if (::WaitForSingleObject(process.hProcess, 30000) != WAIT_OBJECT_0) { ::TerminateProcess(process.hProcess, 99); std::abort(); } @@ -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/GuestFileVectorMemory.cpp b/core/libs/tests/GuestFileVectorMemory.cpp new file mode 100644 index 0000000000..58285bbdd7 --- /dev/null +++ b/core/libs/tests/GuestFileVectorMemory.cpp @@ -0,0 +1,175 @@ +#include "prx/libc/include/General.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libkernel/File/include/FileFlags.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#include +#endif + +struct GuestIovec { + void* base; + std::size_t length; +}; + +extern "C" { +int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); +int APS5_VABI sceKernelClose(int); +std::int64_t APS5_VABI sceKernelLseek(int, std::int64_t, int); +std::int64_t APS5_VABI sceKernelReadv(int, const GuestIovec*, int); +std::int64_t APS5_VABI sceKernelWritev(int, const GuestIovec*, int); +std::int64_t APS5_VABI sceKernelPreadv(int, const GuestIovec*, int, std::int64_t); +std::int64_t APS5_VABI sceKernelPwritev(int, const GuestIovec*, int, std::int64_t); +std::int64_t APS5_VABI readv_nid_postfix(int, const GuestIovec*, int); +std::int64_t APS5_VABI writev_nid_postfix(int, const GuestIovec*, int); +std::int64_t APS5_VABI _readv_nid_postfix(int, const GuestIovec*, int); +std::int64_t APS5_VABI _writev_nid_postfix(int, const GuestIovec*, int); +std::int64_t APS5_VABI preadv_nid_postfix(int, const GuestIovec*, int, std::int64_t); +std::int64_t APS5_VABI pwritev_nid_postfix(int, const GuestIovec*, int, std::int64_t); +int* APS5_VABI __error_nid_postfix(); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "File vector memory check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +struct Transfer { + const char* name; + bool posix; + bool positioned; + bool write; + std::function call; +}; + +static std::uintptr_t observedTable = 0; +static std::size_t observedBytes = 0; + +static void ObserveRead(std::uintptr_t address, std::size_t bytes) { + if (address == observedTable) observedBytes = bytes; +} + +static void Protect(void* address, std::size_t size, bool readable) { +#ifdef _WIN32 + DWORD previous = 0; + Require(VirtualProtect(address, size, readable ? PAGE_READONLY : PAGE_NOACCESS, &previous) != 0); +#else + Require(::mprotect(address, size, readable ? PROT_READ : PROT_NONE) == 0); +#endif +} + +int main(int argc, char** argv) { + const std::array transfers{{ + {"readv", true, false, false, readv_nid_postfix}, + {"writev", true, false, true, writev_nid_postfix}, + {"preadv", true, true, false, [](int fd, const GuestIovec* iov, int count) { return preadv_nid_postfix(fd, iov, count, 1); }}, + {"pwritev", true, true, true, [](int fd, const GuestIovec* iov, int count) { return pwritev_nid_postfix(fd, iov, count, 1); }}, + {"_readv", true, false, false, _readv_nid_postfix}, + {"_writev", true, false, true, _writev_nid_postfix}, + {"sceKernelReadv", false, false, false, sceKernelReadv}, + {"sceKernelWritev", false, false, true, sceKernelWritev}, + {"sceKernelPreadv", false, true, false, [](int fd, const GuestIovec* iov, int count) { return sceKernelPreadv(fd, iov, count, 1); }}, + {"sceKernelPwritev", false, true, true, [](int fd, const GuestIovec* iov, int count) { return sceKernelPwritev(fd, iov, count, 1); }}, + }}; +#ifdef _WIN32 + SYSTEM_INFO system{}; + GetSystemInfo(&system); + const std::size_t page = system.dwPageSize; + auto* memory = static_cast(VirtualAlloc(nullptr, page * 2, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + Require(memory != nullptr); +#else + const auto page = static_cast(::sysconf(_SC_PAGESIZE)); + auto* memory = static_cast(::mmap(nullptr, page * 2, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); + Require(memory != MAP_FAILED); +#endif + std::array bytes{'A', 'B'}; + const std::array vectors{{{bytes.data(), 1}, {bytes.data() + 1, 1}}}; + std::memcpy(memory, vectors.data(), sizeof(vectors)); + std::memcpy(memory + page - sizeof(GuestIovec), vectors.data(), sizeof(GuestIovec)); + Protect(memory, page, true); + Protect(memory + page, page, false); + const auto path = std::filesystem::temp_directory_path() / + ("anyps5-iovec-memory-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + AddPathAlias_nid_no_patch("/anyps5-iovec-memory", path.parent_path().string().c_str()); + const auto guestPath = "/anyps5-iovec-memory/" + path.filename().string(); + for (const auto& transfer : transfers) { + if (argc > 1 && transfer.name != std::string(argv[1])) continue; + std::fprintf(stderr, "%s\n", transfer.name); + { std::ofstream file(path, std::ios::binary); file << "ABCD"; Require(static_cast(file)); } + const int fd = sceKernelOpen(guestPath.c_str(), SCE_KERNEL_O_RDWR, 0); + Require(fd >= 0); + const std::array invalid{{ + nullptr, + reinterpret_cast(std::uintptr_t{1}), + reinterpret_cast(memory + page), + reinterpret_cast(memory + page - sizeof(GuestIovec)), + reinterpret_cast(memory + page - sizeof(GuestIovec) / 2), + reinterpret_cast(UINTPTR_MAX - sizeof(GuestIovec) + 1), + }}; + for (const auto* table : invalid) { + *__error_nid_postfix() = 0; + const auto expected = transfer.posix ? -1 : static_cast(0x8002000eu); + Require(transfer.call(fd, table, 2) == expected); + if (transfer.posix) Require(*__error_nid_postfix() == 14); + Require(sceKernelLseek(fd, 0, SEEK_CUR) == 0); + Require((bytes == std::array{'A', 'B'})); + std::ifstream file(path, std::ios::binary); + std::string contents; + file >> contents; + Require(contents == "ABCD"); + Require(transfer.call(fd, table, 0) == 0); + const auto invalidCount = transfer.posix ? -1 : static_cast(0x80020016u); + Require(transfer.call(fd, table, -1) == invalidCount); + if (transfer.posix) Require(*__error_nid_postfix() == 22); + Require(transfer.call(fd, table, 1025) == invalidCount); + if (transfer.posix) Require(*__error_nid_postfix() == 22); + } + observedTable = reinterpret_cast(memory); + observedBytes = 0; + GuestArena::GuestArenaSetHostReadHook_nid_no_patch(ObserveRead); + Require(transfer.call(fd, reinterpret_cast(memory), 2) == 2); + GuestArena::GuestArenaSetHostReadHook_nid_no_patch(nullptr); + Require(observedBytes == sizeof(vectors)); + Require(sceKernelLseek(fd, 0, SEEK_CUR) == (transfer.positioned ? 0 : 2)); + Require(std::memcmp(memory, vectors.data(), sizeof(vectors)) == 0); + if (transfer.write) { + std::ifstream file(path, std::ios::binary); + std::string contents; + file >> contents; + Require(contents == (transfer.positioned ? "AABD" : "ABCD")); + } else { + Require((bytes == (transfer.positioned ? std::array{'B', 'C'} : std::array{'A', 'B'}))); + } + Require(sceKernelLseek(fd, 0, SEEK_SET) == 0); + Require(transfer.call(fd, reinterpret_cast(memory + page - sizeof(GuestIovec)), 1) == 1); + Require(sceKernelLseek(fd, 0, SEEK_CUR) == (transfer.positioned ? 0 : 1)); + Require(sceKernelLseek(fd, 0, SEEK_SET) == 0); + std::array maximum{}; + maximum.back() = {bytes.data(), 1}; + Require(transfer.call(fd, maximum.data(), static_cast(maximum.size())) == 1); + Require(sceKernelLseek(fd, 0, SEEK_CUR) == (transfer.positioned ? 0 : 1)); + Require(sceKernelClose(fd) == 0); + bytes = {'A', 'B'}; + } + Require(std::filesystem::remove(path)); +#ifdef _WIN32 + Require(VirtualFree(memory, 0, MEM_RELEASE) != 0); +#else + Require(::munmap(memory, page * 2) == 0); +#endif +} diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index b88e6634d4..c1a1a863bb 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -3,6 +3,8 @@ #include "prx/libc/include/General.hpp" #include "prx/libc/include/FileStream.hpp" #include "prx/libkernel/File/include/File.hpp" +#include "prx/libkernel/File/include/NativeStat.hpp" +#include "prx/libkernel/KernelErrors.hpp" #include #include #include @@ -33,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); @@ -43,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); @@ -129,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); @@ -225,6 +238,16 @@ static void TestParentTraversal() { checkInvalid("/parent-test-mount/present.txt/..", 20); FileStat status{}; Require(sceKernelStat("/parent-test-mount/existing/../present.txt", &status) == 0 && status.st_size == 9); + for (const char* name : {"/parent-test-mount/present.txt/.", "/parent-test-mount/present.txt/./"}) { + Require(stat_nid_postfix(name, &status) == -1 && *__error_nid_postfix() == 20); + Require(sceKernelStat(name, &status) == static_cast(0x80020014u)); + Require(access_nid_postfix(name, 0) == -1 && *__error_nid_postfix() == 20); + Require(open_nid_postfix(name, 0, 0) == -1 && *__error_nid_postfix() == 20); + Require(mkdir_nid_postfix(name, 0755) == -1 && *__error_nid_postfix() == 20); + } + Require(stat_nid_postfix("/parent-test-mount/missing/.", &status) == -1 && *__error_nid_postfix() == 2); + Require(stat_nid_postfix("/parent-test-mount/existing/.", &status) == 0 && (status.st_mode & 0170000) == 0040000); + Require(access_nid_postfix("/parent-test-mount/existing/./", 0) == 0); #ifndef _WIN32 for (const char* name : {"slash-unlink", "slash-remove", "slash-rename", "slash-open", "slash-kernel"}) std::ofstream(root / name) << "kept"; Require(unlink_nid_postfix("/parent-test-mount/slash-unlink/") == -1 && *__error_nid_postfix() == 20); @@ -309,6 +332,42 @@ static void TestRelinkedModuleNameCase() { } #ifdef _WIN32 +static void TestCaseSensitiveHostDirectory() { + const auto root = std::filesystem::temp_directory_path() / ("anyps5-sensitive-" + + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directory(root)); + const auto handle = CreateFileW(root.c_str(), FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + Require(handle != INVALID_HANDLE_VALUE); + struct { ULONG flags; } information{FILE_CS_FLAG_CASE_SENSITIVE_DIR}; + constexpr auto caseSensitiveInfo = static_cast(23); + const bool enabled = SetFileInformationByHandle(handle, caseSensitiveInfo, &information, sizeof(information)); + const auto error = GetLastError(); + CloseHandle(handle); + if (!enabled && (error == ERROR_NOT_SUPPORTED || error == ERROR_INVALID_PARAMETER || error == ERROR_ACCESS_DENIED)) { + std::printf("case-sensitive host directory unavailable: %lu\n", error); + std::filesystem::remove(root); + return; + } + Require(enabled); + { std::ofstream file(root / "File.txt"); file << "payload"; } + Require(std::filesystem::create_directory(root / "Sub")); + Require(std::filesystem::create_directory(root / "Both")); + Require(std::filesystem::create_directory(root / "both")); + { std::ofstream file(root / "libCase.prx.guest.prx"); file << "module"; } + const auto host = root.string(); + AddPathAlias_nid_no_patch("/sensitive-host", host.c_str()); + Require(ResolvePath_nid_no_patch("/sensitive-host/file.txt") == root / "File.txt"); + Require(ResolvePath_nid_no_patch("/sensitive-host/sub") == root / "Sub"); + Require(ResolvePathOrRelinkedModule_nid_no_patch("/sensitive-host/libcase.prx") == root / "libCase.prx.guest.prx"); + bool ambiguous = false; + try { ResolvePath_nid_no_patch("/sensitive-host/BOTH/file.txt"); } + catch (const std::runtime_error& failure) { ambiguous = std::strstr(failure.what(), "Ambiguous case-insensitive guest path") != nullptr; } + Require(ambiguous); + RemovePathAlias_nid_no_patch("/sensitive-host"); + std::filesystem::remove_all(root); +} + static std::filesystem::path ExtendedPath(const std::filesystem::path& path) { const auto& native = path.native(); if (native.starts_with(L"\\\\?\\") || native.starts_with(L"\\\\.\\")) return path; @@ -399,6 +458,7 @@ int main() { TestRelinkedModuleNames(); TestRelinkedModuleNameCase(); #ifdef _WIN32 + TestCaseSensitiveHostDirectory(); TestLongHostPathStat(); #endif #ifdef __linux__ @@ -541,6 +601,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"; @@ -559,6 +622,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"; } @@ -730,7 +815,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); @@ -778,6 +863,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); @@ -810,11 +903,24 @@ int main() { Require(sceKernelCheckReachability("") == static_cast(0x80020002u)); Require(sceKernelRename("", presentName.c_str()) == static_cast(0x80020002u)); Require(sceKernelRename(presentName.c_str(), "") == static_cast(0x80020002u)); + Require(sceKernelRename(missingName.c_str(), nullptr) == static_cast(0x80020002u)); + Require(sceKernelRename("", nullptr) == static_cast(0x80020002u)); + Require(sceKernelRename(".", nullptr) == static_cast(0x80020016u)); + Require(sceKernelRename(missingName.c_str(), ".") == static_cast(0x80020002u)); + Require(sceKernelRename(presentName.c_str(), nullptr) == static_cast(0x8002000Eu)); + Require(sceKernelRename(nullptr, presentName.c_str()) == static_cast(0x8002000Eu)); + Require(rename_nid_postfix(missingName.c_str(), nullptr) == -1 && *__error_nid_postfix() == 2); + Require(rename_nid_postfix(presentName.c_str(), nullptr) == -1 && *__error_nid_postfix() == 14); Require(sceKernelRmdir("") == static_cast(0x80020002u)); Require(sceKernelMkdir("", 0700) == static_cast(0x80020002u)); 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; @@ -850,6 +956,11 @@ int main() { Require(stat_nid_postfix(link.string().c_str(), &status) == 0 && (status.st_mode & 0170000) == 0100000 && status.st_size == 5); const auto dangling = root / "dangling"; std::filesystem::create_symlink("missing.txt", dangling); + const auto statLoop = root / "stat-loop"; + std::filesystem::create_symlink("stat-loop", statLoop); + errno = 0; + Require(!File::TryFillFileStat(statLoop, &status) && errno == ELOOP); + Require(sceKernelStat(statLoop.generic_string().c_str(), &status) == SCE_KERNEL_ERROR_ELOOP); Require(lstat_nid_postfix(dangling.string().c_str(), &linkStatus) == 0 && (linkStatus.st_mode & 0170000) == 0120000); constexpr int noFollow = 0x100; Require(sceKernelOpen(link.string().c_str(), noFollow, 0) == static_cast(0x8002001Fu)); @@ -869,7 +980,8 @@ int main() { Require(unlink_nid_postfix(viaMissing.c_str()) == -1 && *__error_nid_postfix() == 2 && std::filesystem::exists(present)); Require(stat_nid_postfix((presentName + "/..").c_str(), &status) == -1 && *__error_nid_postfix() == 20); Require(sceKernelStat((presentName + "/../present.txt").c_str(), &status) == static_cast(0x80020014u)); - Require(unlink_nid_postfix(link.string().c_str()) == 0 && unlink_nid_postfix(dangling.string().c_str()) == 0); + Require(unlink_nid_postfix(link.string().c_str()) == 0 && unlink_nid_postfix(dangling.string().c_str()) == 0 && + unlink_nid_postfix(statLoop.string().c_str()) == 0); #else const auto fileLink = root / "file-link"; if (CreateSymbolicLinkW(fileLink.c_str(), L"present.txt", SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) == 0) { @@ -886,6 +998,12 @@ int main() { Require(CreateSymbolicLinkW(danglingLink.c_str(), L"missing.txt", SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0); Require(CreateSymbolicLinkW(absoluteLink.c_str(), std::filesystem::absolute(present).c_str(), SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0); + const auto statLoop = root / "stat-loop"; + Require(CreateSymbolicLinkW(statLoop.c_str(), L"stat-loop", SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0); + Require(sceKernelStat(statLoop.generic_string().c_str(), &status) == static_cast(0x8002003eu)); + Require(sceKernelOpen(statLoop.generic_string().c_str(), 0, 0) == static_cast(0x8002003eu)); + Require(open_nid_postfix(statLoop.string().c_str(), 0, 0) == -1 && *__error_nid_postfix() == 62); + Require(fopen_nid_postfix(statLoop.string().c_str(), "r") == nullptr && *__error_nid_postfix() == 62); const auto checkRelativeLink = [](const std::filesystem::path& link, std::int64_t targetLength) { FileStat own{}; return lstat_nid_postfix(link.string().c_str(), &own) == 0 && (own.st_mode & 0170000) == 0120000 && @@ -904,7 +1022,10 @@ int main() { Require(lstat_nid_postfix(absoluteLink.string().c_str(), &absoluteStatus) == 0); Require((absoluteStatus.st_mode & 0170000) == 0120000 && absoluteStatus.st_size > 0 && absoluteStatus.st_nlink == 1); Require(stat_nid_postfix(absoluteLink.string().c_str(), &followed) == 0 && followed.st_ino == status.st_ino); + Require(sceKernelOpen((statLoop.generic_string() + "/inside").c_str(), 0, 0) == static_cast(0x8002003eu)); + Require(open_nid_postfix((statLoop.string() + "\\inside").c_str(), 0, 0) == -1 && *__error_nid_postfix() == 62); Require(std::filesystem::remove(fileLink) && std::filesystem::remove(directoryLink) && DeleteFileW(danglingLink.c_str()) != 0 && + DeleteFileW(statLoop.c_str()) != 0 && std::filesystem::remove(absoluteLink) && std::filesystem::remove(targetDirectory)); } const auto junctionTarget = root / "junction-target"; @@ -917,6 +1038,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{}; @@ -960,6 +1086,16 @@ 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(lseek_nid_postfix(-1, 0, 7) == -1 && *__error_nid_postfix() == 9); + Require(lseek_nid_postfix(0x7fffffff, 0, -1) == -1 && *__error_nid_postfix() == 9); + Require(lseek_nid_postfix(locked, 0, 7) == -1 && *__error_nid_postfix() == 22); + 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); @@ -1041,6 +1177,19 @@ int main() { Require(sceKernelOpen(presentName.c_str(), 0x20402, 0) == static_cast(0x80020014u)); Require(std::filesystem::file_size(present) == 5); Require(sceKernelOpen(missingName.c_str(), 0x20000, 0) == static_cast(0x80020002u)); + const auto invalidFlagsName = (root / "invalid-flags.txt").string(); + Require(sceKernelOpen(presentName.c_str(), 3, 0) == static_cast(0x80020016u)); + Require(sceKernelOpen(missingName.c_str(), 0x203, 0600) == static_cast(0x80020016u)); + Require(open_nid_postfix(presentName.c_str(), 3, 0) == -1 && *__error_nid_postfix() == 22); + Require(_open_nid_postfix(presentName.c_str(), 0x403) == -1 && *__error_nid_postfix() == 22); + Require(sceKernelOpen(invalidFlagsName.c_str(), 0x20a02, 0600) == static_cast(0x80020016u)); + Require(open_nid_postfix(invalidFlagsName.c_str(), 0x20a00, 0600) == -1 && *__error_nid_postfix() == 22); + Require(!std::filesystem::exists(invalidFlagsName)); + Require(open_nid_postfix(nullptr, 3, 0) == -1 && *__error_nid_postfix() == 22); + Require(open_nid_postfix("", 0x20a00, 0600) == -1 && *__error_nid_postfix() == 22); + Require(_open_nid_postfix(nullptr, 0x403) == -1 && *__error_nid_postfix() == 22); + Require(_open_nid_postfix("", 3) == -1 && *__error_nid_postfix() == 22); + Require(std::filesystem::file_size(present) == 5); Require(open_nid_postfix("", 0, 0) == -1 && *__error_nid_postfix() == 2); Require(_open_nid_postfix("", 0) == -1 && *__error_nid_postfix() == 2); Require(sceKernelOpen("", 0, 0) == static_cast(0x80020002u)); @@ -1096,6 +1245,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/GuestFloatingPoint.cpp b/core/libs/tests/GuestFloatingPoint.cpp new file mode 100644 index 0000000000..3ea9884bb0 --- /dev/null +++ b/core/libs/tests/GuestFloatingPoint.cpp @@ -0,0 +1,221 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int* APS5_VABI __error_nid_postfix(); +void APS5_VABI _Feraise_nid_postfix(int); +int APS5_VABI fegetround_nid_postfix(); +int APS5_VABI fesetround_nid_postfix(int); +int APS5_VABI fetestexcept_nid_postfix(int); +int APS5_VABI feclearexcept_nid_postfix(int); +int APS5_VABI fegetexceptflag_nid_postfix(std::uint16_t*, int); +int APS5_VABI fesetexceptflag_nid_postfix(const std::uint16_t*, int); +int APS5_VABI feenableexcept_nid_postfix(int); +int APS5_VABI fedisableexcept_nid_postfix(int); +int APS5_VABI __feenableexcept_nid_postfix(int); +int APS5_VABI __fedisableexcept_nid_postfix(int); +int APS5_VABI fegettrapenable_nid_postfix(); +int APS5_VABI fesettrapenable_nid_postfix(int); +} + +namespace { + +struct Environment { + std::array x87; + std::uint32_t sse; +}; + +Environment ReadEnvironment() { + Environment state; + asm volatile("fnstenv %0\n\tfldenv %0" : "=m"(state.x87)); + state.sse = _mm_getcsr(); + return state; +} + +void WriteEnvironment(const Environment& state) { + asm volatile("fldenv %0" : : "m"(state.x87)); + _mm_setcsr(state.sse); +} + +void Require(bool value, const char* message) { + if (!value) { + std::fprintf(stderr, "floating-point environment: %s\n", message); + std::abort(); + } +} + +Environment SeedFlags(unsigned x87, unsigned sse) { + auto state = ReadEnvironment(); + state.x87[0] |= 0x3f; + state.x87[1] = (state.x87[1] & ~0xffu) | x87; + state.sse = (state.sse & ~0x3fu) | (0x3fu << 7) | sse; + WriteEnvironment(state); + return ReadEnvironment(); +} + +void CheckRounding() { + const auto seeded = SeedFlags(0x02, 0x10); + constexpr int modes[] = {0, 0x400, 0x800, 0xc00}; + constexpr int positive[] = {2, 1, 2, 1}; + constexpr int negative[] = {-2, -2, -1, -1}; + for (unsigned index = 0; index < 4; ++index) { + const auto before = ReadEnvironment(); + Require(fesetround_nid_postfix(modes[index]) == 0, "set rounding mode"); + Require(fegetround_nid_postfix() == modes[index], "query rounding mode"); + const auto state = ReadEnvironment(); + Require((state.x87[0] & 0xc00) == static_cast(modes[index]), "x87 rounding bits"); + Require((state.sse & 0x6000) == static_cast(modes[index] << 3), "SSE rounding bits"); + Require((state.x87[0] & ~0xc00u) == (seeded.x87[0] & ~0xc00u), "preserve x87 control bits"); + Require((state.sse & ~0x603fu) == (seeded.sse & ~0x603fu), "preserve SSE control bits"); + Require(state.x87[1] == before.x87[1] && (state.sse & 0x3f) == (before.sse & 0x3f), "rounding preserves exceptions"); + for (const double input : {1.75, -1.75}) { + const int expected = input > 0 ? positive[index] : negative[index]; + int x87; + asm volatile("fldl %1\n\tfistpl %0" : "=m"(x87) : "m"(input) : "st"); + Require(x87 == expected, "x87 arithmetic observes rounding"); + int sse; + asm volatile("cvtsd2si %1, %0" : "=r"(sse) : "x"(input)); + Require(sse == expected, "SSE arithmetic observes rounding"); + } + } + for (const int invalid : {-1, 1, 0x200, 0x1000, 0x7fffffff}) { + const auto before = ReadEnvironment(); + Require(fesetround_nid_postfix(invalid) == -1, "reject invalid rounding mode"); + const auto after = ReadEnvironment(); + Require(before.x87[0] == after.x87[0] && before.sse == after.sse, "invalid mode leaves state unchanged"); + } + auto distinct = ReadEnvironment(); + distinct.x87[0] = (distinct.x87[0] & ~0xc00u) | 0x400; + distinct.sse = (distinct.sse & ~0x6000u) | 0x4000; + WriteEnvironment(distinct); + Require(fegetround_nid_postfix() == 0x400, "query uses the x87 rounding mode"); +} + +void CheckFeraise() { + for (unsigned requested = 0; requested <= 0x3f; ++requested) { + const auto seeded = SeedFlags(0, 0); + *__error_nid_postfix() = 0; + _Feraise_nid_postfix(static_cast(requested)); + const unsigned raised = (requested & 0x18) != 0 ? requested | 0x20 : requested; + const auto state = ReadEnvironment(); + Require((state.x87[1] & 0x3f) == raised && (state.sse & 0x3f) == raised, "_Feraise raises the flags on both units"); + Require(state.x87[0] == seeded.x87[0] && (state.sse & ~0x3fu) == (seeded.sse & ~0x3fu), "_Feraise preserves control bits"); + const int error = (raised & 0x01) != 0 || (raised & 0x1c) == 0 ? 33 : 34; + Require(*__error_nid_postfix() == error, "_Feraise sets EDOM or ERANGE"); + } + SeedFlags(0, 0); +} + +void CheckFlags() { + const auto initial = SeedFlags(0x03, 0x24); + Require(fetestexcept_nid_postfix(0) == 0, "empty exception query"); + Require(fetestexcept_nid_postfix(0x3f) == 0x27, "combine x87 and SSE flags, including denormals"); + Require(fetestexcept_nid_postfix(0x0c) == 0x04, "query only requested flags"); + struct { std::uint16_t before, value, after; } saved{0x1234, 0xffff, 0x5678}; + Require(fegetexceptflag_nid_postfix(&saved.value, 0x07) == 0 && saved.value == 0x07, "save selected flags"); + Require(saved.before == 0x1234 && saved.after == 0x5678, "fexcept_t writes exactly two bytes"); + auto state = ReadEnvironment(); + Require(state.x87[1] == initial.x87[1] && state.sse == initial.sse, "queries do not change flags"); + + const std::uint16_t flags = 0x22; + Require(fesetexceptflag_nid_postfix(&flags, 0x0a) == 0, "restore selected flags"); + state = ReadEnvironment(); + Require((state.x87[1] & 0x3f) == 0x03 && (state.sse & 0x3f) == 0x26, "restore only selected flags on both units"); + Require(fesetexceptflag_nid_postfix(&flags, 0) == 0, "empty restore"); + Require(feclearexcept_nid_postfix(0x03) == 0, "clear selected flags"); + state = ReadEnvironment(); + Require((state.x87[1] & 0x3f) == 0 && (state.sse & 0x3f) == 0x24, "clear flags on both units"); + Require(state.x87[0] == initial.x87[0], "flag operations preserve x87 control"); + Require((state.sse & ~0x3fu) == (initial.sse & ~0x3fu), "flag operations preserve SSE control"); + Require(feclearexcept_nid_postfix(0) == 0 && fetestexcept_nid_postfix(0x3f) == 0x24, "empty clear"); + SeedFlags(0x3f, 0x3f); + Require(feclearexcept_nid_postfix(0x3f) == 0 && fetestexcept_nid_postfix(0x3f) == 0, "clear every exception"); + Require(fegetexceptflag_nid_postfix(&saved.value, 0) == 0 && saved.value == 0, "save empty mask"); + auto traps = ReadEnvironment(); + traps.x87[0] &= ~0x08u; + traps.sse &= ~(0x08u << 7); + WriteEnvironment(traps); + const std::uint16_t none = 0; + Require(fesetexceptflag_nid_postfix(&none, 0x01) == 0, "restore with an unmasked exception"); + Require(feclearexcept_nid_postfix(0x02) == 0, "clear with an unmasked exception"); + state = ReadEnvironment(); + Require(state.x87[0] == traps.x87[0] && state.sse == traps.sse, "flag updates preserve exception masks"); + SeedFlags(0, 0); + for (const bool get : {false, true}) { + bool threw = false; + try { + if (get) fegetexceptflag_nid_postfix(nullptr, 0x3f); + else fesetexceptflag_nid_postfix(nullptr, 0x3f); + } catch (const std::invalid_argument&) { threw = true; } + Require(threw, "reject null exception storage"); + } + CheckFeraise(); +} + +void CheckThreads() { + Require(fesetround_nid_postfix(0) == 0, "reset main rounding mode"); + Require(feclearexcept_nid_postfix(0x3f) == 0, "reset main exceptions"); + std::barrier ready(2); + const auto worker = [&](int rounding, std::uint16_t flags) { + Require(fesetround_nid_postfix(rounding) == 0, "thread rounding mode"); + Require(fesetexceptflag_nid_postfix(&flags, 0x3f) == 0, "thread exceptions"); + ready.arrive_and_wait(); + Require(fegetround_nid_postfix() == rounding, "thread-local rounding mode"); + Require(fetestexcept_nid_postfix(0x3f) == flags, "thread-local exception flags"); + }; + std::thread first(worker, 0x400, 0x01); + std::thread second(worker, 0x800, 0x04); + first.join(); + second.join(); + Require(fegetround_nid_postfix() == 0 && fetestexcept_nid_postfix(0x3f) == 0, "worker state does not change main"); +} + +} + +unsigned X87Masks() { + return ReadEnvironment().x87[0] & 0x3fu; +} + +unsigned SseMasks() { + return (ReadEnvironment().sse >> 7) & 0x3fu; +} + +void CheckTraps() { + const auto seeded = SeedFlags(0, 0); + Require(feenableexcept_nid_postfix(0x49) == static_cast(0xffffffc0u), "enable reports no previous trap as the console does"); + Require(X87Masks() == 0x36 && fegettrapenable_nid_postfix() == 0x09, "enable unmasks the x87 exceptions"); + Require(SseMasks() == 0x36, "enable unmasks the SSE exceptions"); + Require(fedisableexcept_nid_postfix(0x41) == static_cast(0xffffffc9u), "disable returns the traps enabled on both units"); + Require(X87Masks() == 0x37 && fegettrapenable_nid_postfix() == 0x08, "disable masks the x87 exception"); + Require(SseMasks() == 0x37, "disable masks the SSE exception"); + Require(__feenableexcept_nid_postfix(0x02) == static_cast(0xffffffc8u) && X87Masks() == 0x35, "the internal enable differs"); + Require(__fedisableexcept_nid_postfix(0x3f) == static_cast(0xffffffcau) && X87Masks() == 0x3f && fegettrapenable_nid_postfix() == 0, "the internal disable differs"); + Require(fesettrapenable_nid_postfix(0x40) == 1 && X87Masks() == 0x3f, "an out-of-range trap set was accepted"); + Require(fesettrapenable_nid_postfix(0x10004) == 0 && fegettrapenable_nid_postfix() == 0x04 && SseMasks() == 0x3b, "a trap set did not use the low 16 bits"); + Require(fesettrapenable_nid_postfix(0x01) == 0 && fegettrapenable_nid_postfix() == 0x05 && SseMasks() == 0x3a, "a trap set masked enabled traps"); + Require(fesettrapenable_nid_postfix(0) == 0 && fegettrapenable_nid_postfix() == 0x05 && SseMasks() == 0x3a, "an empty trap set masked enabled traps"); + auto mixed = ReadEnvironment(); + mixed.x87[0] = (mixed.x87[0] & ~0x3fu) | 0x36; + mixed.sse = (mixed.sse & ~(0x3fu << 7)) | (0x3au << 7); + WriteEnvironment(mixed); + Require(feenableexcept_nid_postfix(0) == static_cast(0xffffffc1u), "enable reports only the traps enabled on both units"); + Require(fegettrapenable_nid_postfix() == 0x09, "the trap query reads only the x87 control word"); + WriteEnvironment(seeded); +} + +int main() { + const auto original = ReadEnvironment(); + CheckRounding(); + CheckFlags(); + CheckTraps(); + CheckThreads(); + WriteEnvironment(original); + std::puts("floating-point environment checks passed"); +} diff --git a/core/libs/tests/GuestFminf.cpp b/core/libs/tests/GuestFminf.cpp index 742fad398f..fcf48171a6 100644 --- a/core/libs/tests/GuestFminf.cpp +++ b/core/libs/tests/GuestFminf.cpp @@ -1,4 +1,5 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include @@ -40,6 +41,12 @@ static void CheckMinAndMax(T (APS5_VABI *minimum)(T, T), T (APS5_VABI *maximum)( Require(maximum(T(4.0), nan) == T(4.0)); Require(std::isnan(maximum(nan, nan))); + const auto signaling = std::numeric_limits::signaling_NaN(); + std::feclearexcept(FE_ALL_EXCEPT); + Require(minimum(signaling, T(4.0)) == T(4.0) && minimum(T(4.0), signaling) == T(4.0)); + Require(maximum(signaling, T(4.0)) == T(4.0) && maximum(T(4.0), signaling) == T(4.0)); + Require(std::fetestexcept(FE_INVALID) == 0); + Require(std::signbit(minimum(T(-0.0), T(+0.0)))); Require(std::signbit(minimum(T(+0.0), T(-0.0)))); Require(std::signbit(minimum(T(-0.0), T(-0.0)))); diff --git a/core/libs/tests/GuestFont.cpp b/core/libs/tests/GuestFont.cpp index 390074ca1e..80184223b8 100644 --- a/core/libs/tests/GuestFont.cpp +++ b/core/libs/tests/GuestFont.cpp @@ -34,6 +34,8 @@ int APS5_VABI sceFontSetScriptLanguage(FontHandle, int, int); int APS5_VABI sceFontGetScriptLanguage(FontHandle, int, int*); int APS5_VABI sceFontSetTypographicDesign(FontHandle, int, int); int APS5_VABI sceFontGetTypographicDesign(FontHandle, int, int*); +int APS5_VABI sceFontDefineAttribute(FontHandle, int, int*); +int APS5_VABI sceFontGetAttribute(FontHandle, int, int*); int APS5_VABI sceFontSetResolutionDpi(FontHandle, std::uint32_t, std::uint32_t); int APS5_VABI sceFontGetResolutionDpi(FontHandle, std::uint32_t*, std::uint32_t*); int APS5_VABI sceFontSetScalePixel(FontHandle, float, float); @@ -47,7 +49,8 @@ int APS5_VABI sceFontGetCharGlyphCode(FontHandle, std::uint32_t, std::uint32_t*) int APS5_VABI sceFontGetFontResolution(FontHandle, std::uint32_t*, float*); int APS5_VABI sceFontGetRenderScaledKerning(FontHandle, std::uint32_t, std::uint32_t, FontKerning*); int APS5_VABI sceFontGenerateCharGlyph(FontHandle, std::uint32_t, const FontGenerateGlyphDetail*, FontGlyph*); -int APS5_VABI sceFontGlyphDefineAttribute(FontGlyph, std::uint32_t, std::uint64_t); +int APS5_VABI sceFontGlyphDefineAttribute(FontGlyph, int, int*); +int APS5_VABI sceFontGlyphGetAttribute(FontGlyph, int, int*); int APS5_VABI sceFontDeleteGlyph(const FontMemory*, FontGlyph*); int APS5_VABI sceFontTextSourceInit(FontTextSource*, const void*, std::uint32_t, FontTextParseFunction, void*); int APS5_VABI sceFontCreateString(const FontMemory*, FontTextSource*, const FontCreateStringDetail*, FontString*); @@ -286,6 +289,28 @@ int main() { Require(sceFontGetTypographicDesign(font, 1, &setting) == SCE_FONT_OK && setting == 2); Require(sceFontGetTypographicDesign(font, 5, &setting) == SCE_FONT_OK && setting == 0); Require(sceFontGetTypographicDesign(nullptr, 1, &setting) == SCE_FONT_ERROR_INVALID_FONT_HANDLE); + int attribute = -1; + Require(sceFontGetAttribute(font, 0x11, &attribute) == SCE_FONT_OK && attribute == 0); + Require(sceFontDefineAttribute(font, 0x11, &attribute) == SCE_FONT_OK && attribute == 0); + attribute = -1; + Require(sceFontGetAttribute(font, 0x99, &attribute) == SCE_FONT_OK && attribute == 0x11); + Require(sceFontDefineAttribute(font, 0, &attribute) == SCE_FONT_OK && attribute == 0x11); + Require(sceFontDefineAttribute(font, 5, nullptr) == SCE_FONT_OK); + attribute = -1; + Require(sceFontGetAttribute(font, 0, &attribute) == SCE_FONT_OK && attribute == 5); + Require(sceFontGetAttribute(font, 0x11, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + attribute = -1; + Require(sceFontDefineAttribute(nullptr, 0x11, &attribute) == SCE_FONT_ERROR_INVALID_FONT_HANDLE && attribute == -1); + Require(sceFontGetAttribute(nullptr, 0x11, &attribute) == SCE_FONT_ERROR_INVALID_FONT_HANDLE && attribute == -1); + FontHandle otherFont = nullptr; + Require(sceFontOpenFontMemory(library, fontData.data(), static_cast(fontData.size()), nullptr, &otherFont) == SCE_FONT_OK && otherFont != nullptr); + attribute = -1; + Require(sceFontGetAttribute(otherFont, 0, &attribute) == SCE_FONT_OK && attribute == 0); + Require(sceFontDefineAttribute(otherFont, 7, &attribute) == SCE_FONT_OK && attribute == 0); + attribute = -1; + Require(sceFontGetAttribute(otherFont, 0, &attribute) == SCE_FONT_OK && attribute == 7); + Require(sceFontGetAttribute(font, 0, &attribute) == SCE_FONT_OK && attribute == 5); + Require(sceFontCloseFont(otherFont) == SCE_FONT_OK); std::uint32_t hDpi = 1; std::uint32_t vDpi = 1; Require(sceFontGetResolutionDpi(font, &hDpi, &vDpi) == SCE_FONT_OK && hDpi == 72 && vDpi == 72); @@ -348,10 +373,30 @@ int main() { Require(sceFontSetScalePixel(font, 100.0f, 100.0f) == SCE_FONT_OK); FontGlyph glyph = nullptr; Require(sceFontGenerateCharGlyph(font, 'A', nullptr, &glyph) == SCE_FONT_OK && glyph != nullptr); - Require(sceFontGlyphDefineAttribute(glyph, 0x11, 0) == SCE_FONT_OK); - Require(sceFontGlyphDefineAttribute(nullptr, 0x11, 0) == SCE_FONT_ERROR_INVALID_GLYPH); + int glyphAttribute = -1; + Require(sceFontGlyphDefineAttribute(glyph, 0x11, &glyphAttribute) == SCE_FONT_OK && glyphAttribute == 0); + Require(sceFontGlyphGetAttribute(glyph, 0x11, &glyphAttribute) == SCE_FONT_OK && glyphAttribute == 0x11); + glyphAttribute = -1; + Require(sceFontGlyphDefineAttribute(glyph, 0, &glyphAttribute) == SCE_FONT_OK && glyphAttribute == 0x11); + Require(sceFontGlyphDefineAttribute(glyph, 5, nullptr) == SCE_FONT_OK); + glyphAttribute = -1; + Require(sceFontGlyphGetAttribute(glyph, 0, &glyphAttribute) == SCE_FONT_OK && glyphAttribute == 5); + Require(sceFontGlyphGetAttribute(glyph, 0x11, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + glyphAttribute = -1; + Require(sceFontGlyphDefineAttribute(nullptr, 0x11, &glyphAttribute) == SCE_FONT_ERROR_INVALID_GLYPH && glyphAttribute == -1); + Require(sceFontGlyphGetAttribute(nullptr, 0x11, &glyphAttribute) == SCE_FONT_ERROR_INVALID_GLYPH && glyphAttribute == -1); FontGlyphOpaque notAGlyph{}; - Require(sceFontGlyphDefineAttribute(¬AGlyph, 0x11, 0) == SCE_FONT_ERROR_INVALID_GLYPH); + Require(sceFontGlyphDefineAttribute(¬AGlyph, 0x11, &glyphAttribute) == SCE_FONT_ERROR_INVALID_GLYPH); + Require(sceFontGlyphGetAttribute(¬AGlyph, 0x11, &glyphAttribute) == SCE_FONT_ERROR_INVALID_GLYPH); + FontGlyph otherGlyph = nullptr; + Require(sceFontGenerateCharGlyph(font, 'V', nullptr, &otherGlyph) == SCE_FONT_OK && otherGlyph != nullptr); + glyphAttribute = -1; + Require(sceFontGlyphGetAttribute(otherGlyph, 0, &glyphAttribute) == SCE_FONT_OK && glyphAttribute == 0); + Require(sceFontGlyphDefineAttribute(otherGlyph, 9, &glyphAttribute) == SCE_FONT_OK && glyphAttribute == 0); + glyphAttribute = -1; + Require(sceFontGlyphGetAttribute(otherGlyph, 0, &glyphAttribute) == SCE_FONT_OK && glyphAttribute == 9); + Require(sceFontGlyphGetAttribute(glyph, 0, &glyphAttribute) == SCE_FONT_OK && glyphAttribute == 5); + Require(sceFontDeleteGlyph(&memory, &otherGlyph) == SCE_FONT_OK && otherGlyph == nullptr); Require(sceFontDeleteGlyph(&memory, &glyph) == SCE_FONT_OK && glyph == nullptr); FontKerning kerning{1.0f, 1.0f, 1.0f, 1.0f}; Require(sceFontGetKerning(font, 'A', 'V', &kerning) == SCE_FONT_OK && KerningIs(kerning, -20.0f)); diff --git a/core/libs/tests/GuestFreeType.cpp b/core/libs/tests/GuestFreeType.cpp index 246b5cbb2b..bfb51cb340 100644 --- a/core/libs/tests/GuestFreeType.cpp +++ b/core/libs/tests/GuestFreeType.cpp @@ -1,3 +1,4 @@ +#include "FreeTypeFontFixture.hpp" #include "prx/libSceFreeType/include/GuestFreeType.hpp" #include "prx/libc/include/General.hpp" @@ -46,65 +47,6 @@ template static bool ThrowsWith(TCall call, const std::string& return false; } -static void Put16(std::vector& out, std::uint32_t value) { - out.push_back(static_cast(value >> 8)); - out.push_back(static_cast(value)); -} - -static void Put32(std::vector& out, std::uint32_t value) { - Put16(out, value >> 16); - Put16(out, value & 0xFFFFu); -} - -static void PutAll16(std::vector& out, std::initializer_list values) { - for (const int value : values) Put16(out, static_cast(static_cast(value))); -} - -static std::vector SquareGlyphFont(std::uint32_t codepoint, int width) { - std::vector head; - Put32(head, 0x00010000u); - Put32(head, 0x00010000u); - Put32(head, 0); - Put32(head, 0x5F0F3CF5u); - PutAll16(head, {0x000B, 1000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, width, 800, 0, 8, 2, 0, 0}); - std::vector hhea; - Put32(hhea, 0x00010000u); - PutAll16(hhea, {800, -200, 0, width, 0, 0, width, 1, 0, 0, 0, 0, 0, 0, 0, 2}); - std::vector maxp; - Put32(maxp, 0x00010000u); - PutAll16(maxp, {2, 4, 1, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0}); - std::vector hmtx; - PutAll16(hmtx, {width, 0, width, 100}); - std::vector glyf; - PutAll16(glyf, {1, 100, 0, width - 100, 700, 3, 0}); - glyf.insert(glyf.end(), {1, 1, 1, 1}); - PutAll16(glyf, {100, 0, width - 200, 0, 0, 700, 0, -700}); - std::vector loca; - PutAll16(loca, {0, 0, static_cast(glyf.size() / 2)}); - std::vector cmap; - PutAll16(cmap, {0, 1, 3, 1}); - Put32(cmap, 12); - PutAll16(cmap, {4, 32, 0, 4, 4, 1, 0, static_cast(codepoint), 0xFFFF, 0, static_cast(codepoint), 0xFFFF, 1 - static_cast(codepoint), 1, 0, 0}); - const std::vector*>> tables = { - {"cmap", &cmap}, {"glyf", &glyf}, {"head", &head}, {"hhea", &hhea}, {"hmtx", &hmtx}, {"loca", &loca}, {"maxp", &maxp}}; - std::vector font; - Put32(font, 0x00010000u); - PutAll16(font, {static_cast(tables.size()), 64, 2, static_cast(tables.size()) * 16 - 64}); - std::uint32_t offset = 12 + static_cast(tables.size()) * 16; - for (const auto& [tag, data] : tables) { - font.insert(font.end(), tag, tag + 4); - Put32(font, 0); - Put32(font, offset); - Put32(font, static_cast(data->size())); - offset += (static_cast(data->size()) + 3u) & ~3u; - } - for (const auto& [tag, data] : tables) { - font.insert(font.end(), data->begin(), data->end()); - font.resize((font.size() + 3u) & ~std::size_t{3}); - } - return font; -} - static void FaceMatches(const GuestFaceRec* face, FT_Face host, const void* library) { Require(face->num_faces == host->num_faces && face->face_index == host->face_index && face->face_flags == host->face_flags); Require(face->style_flags == host->style_flags && face->num_glyphs == host->num_glyphs && face->num_glyphs == 2); diff --git a/core/libs/tests/GuestFreeTypeCharmap.cpp b/core/libs/tests/GuestFreeTypeCharmap.cpp new file mode 100644 index 0000000000..27372b1526 --- /dev/null +++ b/core/libs/tests/GuestFreeTypeCharmap.cpp @@ -0,0 +1,143 @@ +#include "FreeTypeFontFixture.hpp" +#include "prx/libSceFreeType/include/GuestFreeType.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include FT_FREETYPE_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace FreeType; + +extern "C" { +int APS5_VABI FT_Init_FreeType_nid_postfix(void**); +int APS5_VABI FT_New_Face_nid_postfix(void*, const char*, std::int64_t, GuestFaceRec**); +std::uint32_t APS5_VABI FT_Get_Char_Index_nid_postfix(GuestFaceRec*, std::uint64_t); +int APS5_VABI FT_Load_Glyph_nid_postfix(GuestFaceRec*, std::uint32_t, std::int32_t); +int APS5_VABI FT_Select_Charmap_nid_postfix(GuestFaceRec*, std::int32_t); +int APS5_VABI FT_Set_Charmap_nid_postfix(GuestFaceRec*, GuestCharMapRec*); +std::uint64_t APS5_VABI FT_Get_First_Char_nid_postfix(GuestFaceRec*, std::uint32_t*); +std::uint64_t APS5_VABI FT_Get_Next_Char_nid_postfix(GuestFaceRec*, std::uint64_t, std::uint32_t*); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "FreeType charmap check failed at line %d\n", line); + std::abort(); + } +} + +#define Require(value) Check((value), __LINE__) + +static std::vector> Walk(GuestFaceRec* guest, FT_Face host) { + std::vector> result; + std::uint32_t guestIndex = 999; + FT_UInt hostIndex = 999; + std::uint64_t guestCode = FT_Get_First_Char_nid_postfix(guest, &guestIndex); + FT_ULong hostCode = FT_Get_First_Char(host, &hostIndex); + for (int iteration = 0; iteration < 4; ++iteration) { + Require(guestCode == hostCode && guestIndex == hostIndex); + if (guestIndex == 0) return result; + result.emplace_back(guestCode, guestIndex); + guestCode = FT_Get_Next_Char_nid_postfix(guest, guestCode, &guestIndex); + hostCode = FT_Get_Next_Char(host, hostCode, &hostIndex); + } + std::abort(); +} + +int main() { + const auto root = std::filesystem::temp_directory_path() / ("anyps5-freetype-charmap-" + std::to_string(std::random_device{}())); + Require(std::filesystem::create_directory(root)); + std::filesystem::create_directory(root / "app0"); + const auto font = SquareGlyphFont('A', 700, true); + const auto zeroFont = SquareGlyphFont(0, 700); + for (const auto& [name, bytes] : {std::pair{"maps.ttf", &font}, std::pair{"zero.ttf", &zeroFont}}) { + std::ofstream file(root / "app0" / name, std::ios::binary); + file.write(reinterpret_cast(bytes->data()), static_cast(bytes->size())); + Require(static_cast(file)); + } + const auto previous = std::filesystem::current_path(); + std::filesystem::current_path(root); + FT_Library native = nullptr; + FT_Face host = nullptr; + FT_Face foreignHost = nullptr; + FT_Face zeroHost = nullptr; + Require(FT_Init_FreeType(&native) == 0); + Require(FT_New_Memory_Face(native, font.data(), static_cast(font.size()), 0, &host) == 0); + Require(FT_New_Memory_Face(native, font.data(), static_cast(font.size()), 0, &foreignHost) == 0); + Require(FT_New_Memory_Face(native, zeroFont.data(), static_cast(zeroFont.size()), 0, &zeroHost) == 0); + void* library = nullptr; + GuestFaceRec* face = nullptr; + GuestFaceRec* foreign = nullptr; + GuestFaceRec* zero = nullptr; + Require(FT_Init_FreeType_nid_postfix(&library) == 0); + Require(FT_New_Face_nid_postfix(library, "/app0/maps.ttf", 0, &face) == 0); + Require(FT_New_Face_nid_postfix(library, "/app0/maps.ttf", 0, &foreign) == 0); + Require(FT_New_Face_nid_postfix(library, "/app0/zero.ttf", 0, &zero) == 0); + Require(face->num_charmaps == 2 && host->num_charmaps == 2); + const auto unicode = Walk(face, host); + Require(unicode.size() == 1 && unicode[0].first == 'A' && unicode[0].second == 1); + const auto zeroChars = Walk(zero, zeroHost); + Require(zeroChars.size() == 1 && zeroChars[0].first == 0 && zeroChars[0].second == 1); + + std::uint32_t index = 999; + Require(FT_Get_First_Char_nid_postfix(nullptr, &index) == 0 && index == 0); + index = 999; + Require(FT_Get_Next_Char_nid_postfix(nullptr, 0, &index) == 0 && index == 0); + Require(FT_Get_First_Char_nid_postfix(face, nullptr) == FT_Get_First_Char(host, nullptr)); + Require(FT_Get_Next_Char_nid_postfix(face, 0, nullptr) == FT_Get_Next_Char(host, 0, nullptr)); + Require(FT_Select_Charmap_nid_postfix(nullptr, FT_ENCODING_UNICODE) == FT_Err_Invalid_Face_Handle); + Require(FT_Set_Charmap_nid_postfix(nullptr, face->charmaps[0]) == FT_Err_Invalid_Face_Handle); + Require(FT_Load_Glyph_nid_postfix(face, 1, FT_LOAD_NO_SCALE) == 0); + Require(face->glyph->outline.n_points == 4); + auto* points = face->glyph->outline.points; + const auto changedX = ++points[0].x; + + for (const FT_Encoding encoding : {FT_ENCODING_MS_SYMBOL, FT_ENCODING_UNICODE}) { + Require(FT_Select_Charmap_nid_postfix(face, encoding) == FT_Select_Charmap(host, encoding)); + Require(face->charmap->encoding == static_cast(host->charmap->encoding)); + Require(face->glyph->outline.points == points && points[0].x == changedX); + const auto characters = Walk(face, host); + Require(characters.size() == 1 && characters[0].first == (encoding == FT_ENCODING_UNICODE ? 'A' : 0xF041)); + Require(FT_Get_Char_Index_nid_postfix(face, characters[0].first) == characters[0].second); + for (const std::uint64_t code : std::array{0, 64, 65, 0xF041, 0xFFFFFFFF, 0x100000040, std::numeric_limits::max()}) { + std::uint32_t guestIndex = 999; + FT_UInt hostIndex = 999; + const auto actual = FT_Get_Next_Char_nid_postfix(face, code, &guestIndex); + const auto expected = FT_Get_Next_Char(host, static_cast(code), &hostIndex); + Require(actual == expected && guestIndex == hostIndex); + } + const auto selected = face->charmap; + Require(FT_Select_Charmap_nid_postfix(face, FT_ENCODING_NONE) == FT_Select_Charmap(host, FT_ENCODING_NONE)); + Require(face->charmap == selected); + Require(FT_Set_Charmap_nid_postfix(face, nullptr) == FT_Set_Charmap(host, nullptr)); + Require(face->charmap == selected); + Require(FT_Set_Charmap_nid_postfix(face, foreign->charmaps[0]) == FT_Set_Charmap(host, foreignHost->charmaps[0])); + Require(face->charmap == selected); + FT_CharMapRec fakeHost{}; + GuestCharMapRec fakeGuest{}; + Require(FT_Set_Charmap_nid_postfix(face, &fakeGuest) == FT_Set_Charmap(host, &fakeHost)); + Require(face->charmap == selected); + } + for (int map = 1; map >= 0; --map) { + Require(FT_Set_Charmap_nid_postfix(face, face->charmaps[map]) == 0 && FT_Set_Charmap(host, host->charmaps[map]) == 0); + Require(face->charmap == face->charmaps[map]); + Require(face->glyph->outline.points == points && points[0].x == changedX); + Require(Walk(face, host).size() == 1); + } + + Require(FT_Done_FreeType(native) == 0); + std::filesystem::current_path(previous); + std::error_code ignored; + std::filesystem::remove_all(root, ignored); +} diff --git a/core/libs/tests/GuestG2PDialog.cpp b/core/libs/tests/GuestG2PDialog.cpp index e896a87e42..b3e0cb2b85 100644 --- a/core/libs/tests/GuestG2PDialog.cpp +++ b/core/libs/tests/GuestG2PDialog.cpp @@ -59,4 +59,13 @@ int main() { Require(sceG2PDialogTerminate() == 0); Require(sceG2PDialogTerminate() == kErrNotInitialized); Require(sceG2PDialogGetStatus() == kStatusNone); + + Require(sceG2PDialogInitialize() == 0); + Require(sceG2PDialogGetStatus() == kStatusInitialized); + Require(sceG2PDialogClose() == 0); + Require(sceG2PDialogGetStatus() == kStatusFinished); + result[0] = -1; + Require(sceG2PDialogGetResult(result) == 0); + Require(result[0] == kResultUserCanceled); + Require(sceG2PDialogTerminate() == 0); } 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/GuestGenericCategory.cpp b/core/libs/tests/GuestGenericCategory.cpp index e42958827f..b4f3f98687 100644 --- a/core/libs/tests/GuestGenericCategory.cpp +++ b/core/libs/tests/GuestGenericCategory.cpp @@ -8,7 +8,7 @@ #include #include -struct Category { const void* const* vtable; }; +struct Category { void* const* vtable; }; struct ErrorCode { int value; const Category* category; }; using Message = GuestLocale::String* (APS5_VABI *)(GuestLocale::String*, const Category*, int); using Name = const char* (APS5_VABI *)(const Category*); diff --git a/core/libs/tests/GuestGetitimer.cpp b/core/libs/tests/GuestGetitimer.cpp index 04f037f499..1fa5f82516 100644 --- a/core/libs/tests/GuestGetitimer.cpp +++ b/core/libs/tests/GuestGetitimer.cpp @@ -86,9 +86,18 @@ int main() { Require(GuestAlarm_nid_no_patch(0) > 0, "getitimer cancelled the alarm"); Require(ReadRemaining() == 0, "cancelled alarm is not zero"); - GuestAlarm_nid_no_patch(std::numeric_limits::max()); - Require(ReadRemaining() > static_cast(std::numeric_limits::max()) * 1000000, - "long alarm was truncated to signed 32 bits"); + constexpr unsigned int longestAlarm = std::numeric_limits::max() / 2; + Require(GuestAlarm_nid_no_patch(longestAlarm) == 0, "alarm was already active"); + Require(ReadRemaining() > static_cast(longestAlarm - 1) * 1000000, + "long alarm was truncated"); + for (unsigned int invalid : {longestAlarm + 1, static_cast(std::numeric_limits::max()), std::numeric_limits::max()}) { + *__error_nid_postfix() = 0; + Require(GuestAlarm_nid_no_patch(invalid) == std::numeric_limits::max() && *__error_nid_postfix() == 22, + "an alarm beyond the itimer range did not fail with EINVAL"); + Require(ReadRemaining() > static_cast(longestAlarm - 1) * 1000000, + "a rejected alarm replaced the pending one"); + } + Require(GuestAlarm_nid_no_patch(0) == longestAlarm, "a rejected alarm changed the remaining time"); GuestAlarm_nid_no_patch(1); std::this_thread::sleep_for(std::chrono::milliseconds(1100)); Require(ReadRemaining() == 0, "expired alarm is not zero"); diff --git a/core/libs/tests/GuestGetrlimit.cpp b/core/libs/tests/GuestGetrlimit.cpp index 74a1e34dab..5d6d5d2999 100644 --- a/core/libs/tests/GuestGetrlimit.cpp +++ b/core/libs/tests/GuestGetrlimit.cpp @@ -14,6 +14,9 @@ #include #include #endif +#ifdef __APPLE__ +#include +#endif struct ResourceLimit { std::int64_t rlim_cur; @@ -58,12 +61,9 @@ static ResourceLimit GuestLimit(int resource) { return limit; } -static void CheckOpenFileLimit() { +#if defined(_WIN32) || defined(__APPLE__) +static void RequireDescriptorsEndAt(const ResourceLimit& limit) { constexpr int GUEST_EMFILE = 24; - const ResourceLimit limit = GuestLimit(GUEST_RLIMIT_NOFILE); - Require(limit.rlim_cur == limit.rlim_max); -#ifdef _WIN32 - Require(limit.rlim_cur == 8192); std::vector descriptors; int pair[2]{-1, -1}; *__error_nid_postfix() = 0; @@ -76,6 +76,11 @@ static void CheckOpenFileLimit() { Require(*__error_nid_postfix() == GUEST_EMFILE); Require(descriptors.size() + 64 >= static_cast(limit.rlim_cur)); for (const int descriptor : descriptors) Require(close_nid_postfix(descriptor) == 0); +} +#endif + +#ifdef _WIN32 +static void RequireStreamsOpen() { const auto path = (std::filesystem::temp_directory_path() / ("aps5-open-streams-" + std::to_string(GetCurrentProcessId()))).string(); std::vector streams; for (int index = 0; index < 1024; ++index) { @@ -85,11 +90,28 @@ static void CheckOpenFileLimit() { } for (void* stream : streams) Require(fclose_nid_postfix(stream) == 0); std::filesystem::remove(path); +} +#endif + +static void CheckOpenFileLimit() { + const ResourceLimit limit = GuestLimit(GUEST_RLIMIT_NOFILE); + Require(limit.rlim_cur == limit.rlim_max); +#ifdef _WIN32 + Require(limit.rlim_cur == 8192); + RequireDescriptorsEndAt(limit); + RequireStreamsOpen(); #else rlimit host{}; Require(getrlimit(RLIMIT_NOFILE, &host) == 0); Require(host.rlim_cur == host.rlim_max); Require(host.rlim_cur == RLIM_INFINITY ? limit.rlim_cur == GUEST_RLIM_INFINITY : limit.rlim_cur == static_cast(host.rlim_cur)); +#endif +#ifdef __APPLE__ + int perProcess = 0; + std::size_t size = sizeof(perProcess); + Require(sysctlbyname("kern.maxfilesperproc", &perProcess, &size, nullptr, 0) == 0); + Require(perProcess > 0 && limit.rlim_cur <= perProcess); + RequireDescriptorsEndAt(limit); #endif Require(setrlimit_nid_postfix(GUEST_RLIMIT_NOFILE, &limit) == 0); const ResourceLimit lowered{limit.rlim_max > 1 ? limit.rlim_max - 1 : 1, limit.rlim_max}; @@ -143,17 +165,6 @@ static void LoweredHostLimitIsReported() { } #endif -static void RequireUnsupported(int resource) { - ResourceLimit limit{}; - bool rejected = false; - try { - getrlimit_nid_postfix(resource, &limit); - } catch (const std::runtime_error&) { - rejected = true; - } - Require(rejected); -} - int main() { #ifdef _WIN32 GuestLimit(GUEST_RLIMIT_DATA); @@ -165,6 +176,8 @@ int main() { GUEST_RLIMIT_CPU, GUEST_RLIMIT_FSIZE, GUEST_RLIMIT_CORE, + GUEST_RLIMIT_RSS, + GUEST_RLIMIT_MEMLOCK, GUEST_RLIMIT_SBSIZE, GUEST_RLIMIT_AS, GUEST_RLIMIT_NPTS, @@ -183,14 +196,12 @@ int main() { RequireError(GUEST_RLIM_NLIMITS, nullptr, GUEST_EINVAL); RequireError(-1, nullptr, GUEST_EINVAL); RequireError(GUEST_RLIMIT_DATA, nullptr, GUEST_EFAULT); - for (int resource : { - GUEST_RLIMIT_STACK, - GUEST_RLIMIT_RSS, - GUEST_RLIMIT_MEMLOCK, - GUEST_RLIMIT_NPROC, - }) { - RequireUnsupported(resource); - } + const ResourceLimit stack = GuestLimit(GUEST_RLIMIT_STACK); + Require(stack.rlim_cur == 0x200000 && stack.rlim_max == 0x200000); + RequireError(GUEST_RLIMIT_STACK, nullptr, GUEST_EFAULT); + const ResourceLimit processes = GuestLimit(GUEST_RLIMIT_NPROC); + Require(processes.rlim_cur == 1604 && processes.rlim_max == 1604); + RequireError(GUEST_RLIMIT_NPROC, nullptr, GUEST_EFAULT); const ResourceLimit nofile = GuestLimit(GUEST_RLIMIT_NOFILE); const int tableSize = getdtablesize_nid_postfix(); std::int64_t expectedTableSize = nofile.rlim_cur; 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..4f05d57f02 100644 --- a/core/libs/tests/GuestHmd2.cpp +++ b/core/libs/tests/GuestHmd2.cpp @@ -11,10 +11,52 @@ #include extern "C" { -int APS5_VABI sceHmd2GazeActivateGazeTracking(); -int APS5_VABI sceHmd2ReprojectionGetCurrentOutput(); -int APS5_VABI sceHmd2SetFieldOfView(); +std::int32_t APS5_VABI Hmd2WriteStateTwo(void* a0); +std::int32_t APS5_VABI Hmd2WriteSize0x2000Align0x100(std::uint64_t* a0, std::uint64_t* a1); +std::int32_t APS5_VABI Hmd2WriteFourConstants(void* a0); +std::int32_t APS5_VABI Hmd2WriteSixHalvesThenInvalidHandle(std::uint64_t a0, void* a1); +std::int32_t APS5_VABI Hmd2WriteTwelveHalvesThenInvalidHandle(std::uint64_t a0, void* a1); +std::int32_t APS5_VABI Hmd2SlotUpTo3CheckSecondThirdThenZero(std::uint64_t a0, const void* a1, const void* a2); +std::int32_t APS5_VABI Hmd2SlotUpTo1ThenError1002(std::uint64_t a0); +std::int32_t APS5_VABI Hmd2SlotUpTo1CheckSecondThenError1002(std::uint64_t a0, const void* a1); +std::int32_t APS5_VABI Hmd2SlotUpTo1ThenMinusOne(std::uint64_t a0); +std::int32_t APS5_VABI Hmd2SlotUpTo2ThenMinusOne(std::uint64_t a0); +std::int32_t APS5_VABI Hmd2SlotUpTo2ThenError1002(std::uint64_t a0); +std::int32_t APS5_VABI Hmd2SlotUpTo2ThenError1003(std::uint64_t a0); +std::int32_t APS5_VABI Hmd2SlotUpTo2CheckSecondThenError002C(std::uint64_t a0, const void* a1); +std::int32_t APS5_VABI Hmd2IndexZeroCheckSecondThenZero(std::uint64_t a0, const void* a1); +std::int32_t APS5_VABI Hmd2CheckFirstThenError0004(const void*); +std::int32_t APS5_VABI Hmd2CheckFirstThenZero(const void*); +std::int32_t APS5_VABI Hmd2CheckFirstThenError1002(const void*); +std::int32_t APS5_VABI Hmd2CheckFirstThenMinusOne(const void*); +std::int32_t APS5_VABI Hmd2CheckFirstTwoThenParameterInvalid(const void*, const void*); +std::int32_t APS5_VABI Hmd2CheckSecondThenError002C(const void*, const void*); +std::int32_t APS5_VABI Hmd2CheckFirstThenParameterInvalid(const void*); +std::int32_t APS5_VABI Hmd2CheckAllSixThenZero(const void*, const void*, const void*, const void*, const void*, const void*); +std::int32_t APS5_VABI Hmd2CheckSecondThenParameterInvalid(const void*, const void*); +std::int32_t APS5_VABI Hmd2CheckFirstThenError800d0203(const void*); +std::int32_t APS5_VABI Hmd2CheckFirstThenError1003(const void*); +std::int32_t APS5_VABI Hmd2CheckFirstThenError1010(const void*); +std::int32_t APS5_VABI Hmd2CheckFirstTwoThenError1002(const void*, const void*); +std::int32_t APS5_VABI Hmd2CheckFirstTwoThenError1003(const void*, const void*); +std::int32_t APS5_VABI Hmd2CheckFirstAndThirdThenZero(const void*, const void*, const void*); +std::int32_t APS5_VABI Hmd2CheckFirstAndThirdThenError1003(const void*, const void*, const void*); +std::int32_t APS5_VABI Hmd2CheckSecondThenInvalidHandle(const void*, const void*); +std::int32_t APS5_VABI Hmd2CheckSecondThenError1002(const void*, const void*); +std::int32_t APS5_VABI Hmd2BeforeInitializeZero(); +std::int32_t APS5_VABI Hmd2BeforeInitializeNotInitialized(); +std::int32_t APS5_VABI Hmd2BeforeInitializeError0004(); +std::int32_t APS5_VABI Hmd2BeforeInitializeParameterNull(); +std::int32_t APS5_VABI Hmd2BeforeInitializeParameterInvalid(); +std::int32_t APS5_VABI Hmd2BeforeInitializeReprojectionNotInitialized(); +std::int32_t APS5_VABI Hmd2BeforeInitializeError002C(); +std::int32_t APS5_VABI Hmd2BeforeInitializeError1002(); +std::int32_t APS5_VABI Hmd2BeforeInitializeError1003(); +std::int32_t APS5_VABI Hmd2BeforeInitializeMinusOne(); +std::int32_t APS5_VABI Hmd2BeforeInitializeValue0400(); +std::int32_t APS5_VABI Hmd2BeforeInitializeValue20000(); } + namespace { alignas(16) std::array process{}; @@ -48,6 +90,95 @@ void TestBeforeInitialize() { Require(sceHmd2Open(USER_SERVICE_INITIAL_USER_ID, 0, 0, nullptr) == Hmd2::NotInitialized, "open before initialization"); Require(sceHmd2Close(0) == Hmd2::NotInitialized, "close before initialization"); Require(sceHmd2Terminate() == Hmd2::NotInitialized, "terminate before initialization"); + { + std::uint8_t out[64]; + std::uint64_t words[2]; + Require(Hmd2WriteStateTwo(nullptr) == Hmd2::ParameterNull, "Hmd2WriteStateTwo null before initialization"); + std::memset(out, 0xaa, sizeof out); + Require(Hmd2WriteStateTwo(out) == 0 && out[0] == 2 && out[1] == 0 && out[4] == 0 && out[5] == 0xaa, "Hmd2WriteStateTwo bytes before initialization"); + Require(Hmd2WriteSize0x2000Align0x100(nullptr, words) == Hmd2::ParameterNull && Hmd2WriteSize0x2000Align0x100(words, nullptr) == Hmd2::ParameterNull, "Hmd2WriteSize0x2000Align0x100 null before initialization"); + words[0] = words[1] = ~0ull; + Require(Hmd2WriteSize0x2000Align0x100(words, words + 1) == 0 && words[0] == 0x2000 && words[1] == 0x100, "Hmd2WriteSize0x2000Align0x100 values before initialization"); + Require(Hmd2WriteFourConstants(nullptr) == Hmd2::ParameterNull, "Hmd2WriteFourConstants null before initialization"); + std::memset(out, 0xaa, sizeof out); + Require(Hmd2WriteFourConstants(out) == 0 && out[0] == 0xe1 && out[15] == 0x3c && out[16] == 0xaa, "Hmd2WriteFourConstants bytes before initialization"); + Require(Hmd2WriteSixHalvesThenInvalidHandle(0, nullptr) == Hmd2::ParameterNull, "Hmd2WriteSixHalvesThenInvalidHandle null before initialization"); + std::memset(out, 0xaa, sizeof out); + Require(Hmd2WriteSixHalvesThenInvalidHandle(0, out) == Hmd2::InvalidHandle && out[7] == 0x3f && out[20] == 0x00 && out[23] == 0x3f && out[24] == 0xaa, "Hmd2WriteSixHalvesThenInvalidHandle bytes before initialization"); + Require(Hmd2WriteTwelveHalvesThenInvalidHandle(0, nullptr) == Hmd2::ParameterNull, "Hmd2WriteTwelveHalvesThenInvalidHandle null before initialization"); + std::memset(out, 0xaa, sizeof out); + Require(Hmd2WriteTwelveHalvesThenInvalidHandle(0, out) == Hmd2::InvalidHandle && out[7] == 0x3f && out[31] == 0x3f && out[47] == 0x00 && out[48] == 0xaa, "Hmd2WriteTwelveHalvesThenInvalidHandle bytes before initialization"); + Require(Hmd2SlotUpTo3CheckSecondThirdThenZero(9, nullptr, out) == Hmd2::ParameterNull && Hmd2SlotUpTo3CheckSecondThirdThenZero(0, out, nullptr) == Hmd2::ParameterNull, "Hmd2SlotUpTo3CheckSecondThirdThenZero null before initialization"); + Require(Hmd2SlotUpTo3CheckSecondThirdThenZero(3, out, out) == 0 && Hmd2SlotUpTo3CheckSecondThirdThenZero(4, out, out) == Hmd2::ParameterInvalid && Hmd2SlotUpTo3CheckSecondThirdThenZero(~0ull, out, out) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo3CheckSecondThirdThenZero slots before initialization"); + Require(Hmd2SlotUpTo1ThenError1002(1) == Hmd2::Error1002 && Hmd2SlotUpTo1ThenError1002(2) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo1ThenError1002 before initialization"); + Require(Hmd2SlotUpTo1CheckSecondThenError1002(5, nullptr) == Hmd2::ParameterNull && Hmd2SlotUpTo1CheckSecondThenError1002(0, out) == Hmd2::Error1002 && Hmd2SlotUpTo1CheckSecondThenError1002(2, out) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo1CheckSecondThenError1002 before initialization"); + Require(Hmd2SlotUpTo1ThenMinusOne(1) == -1 && Hmd2SlotUpTo1ThenMinusOne(2) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo1ThenMinusOne before initialization"); + Require(Hmd2SlotUpTo2ThenMinusOne(2) == -1 && Hmd2SlotUpTo2ThenMinusOne(3) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo2ThenMinusOne before initialization"); + Require(Hmd2SlotUpTo2ThenError1002(2) == Hmd2::Error1002 && Hmd2SlotUpTo2ThenError1002(3) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo2ThenError1002 before initialization"); + Require(Hmd2SlotUpTo2ThenError1003(0) == Hmd2::Error1003 && Hmd2SlotUpTo2ThenError1003(16) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo2ThenError1003 before initialization"); + Require(Hmd2SlotUpTo2CheckSecondThenError002C(8, nullptr) == Hmd2::ParameterNull && Hmd2SlotUpTo2CheckSecondThenError002C(2, out) == Hmd2::Error002C && Hmd2SlotUpTo2CheckSecondThenError002C(3, out) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo2CheckSecondThenError002C before initialization"); + Require(Hmd2IndexZeroCheckSecondThenZero(1, nullptr) == Hmd2::ParameterNull && Hmd2IndexZeroCheckSecondThenZero(0, out) == 0 && Hmd2IndexZeroCheckSecondThenZero(1, out) == Hmd2::Error100F, "Hmd2IndexZeroCheckSecondThenZero before initialization"); + } + const std::uint8_t dummyBytes[16]{}; + const void* dummy = dummyBytes; + Require(Hmd2CheckFirstThenError0004(dummy) == Hmd2::Error0004, "Hmd2CheckFirstThenError0004 with every pointer present before initialization"); + Require(Hmd2CheckFirstThenError0004(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenError0004 with pointer 0 null before initialization"); + Require(Hmd2CheckFirstThenZero(dummy) == 0, "Hmd2CheckFirstThenZero with every pointer present before initialization"); + Require(Hmd2CheckFirstThenZero(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenZero with pointer 0 null before initialization"); + Require(Hmd2CheckFirstThenError1002(dummy) == Hmd2::Error1002, "Hmd2CheckFirstThenError1002 with every pointer present before initialization"); + Require(Hmd2CheckFirstThenError1002(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenError1002 with pointer 0 null before initialization"); + Require(Hmd2CheckFirstThenMinusOne(dummy) == -1, "Hmd2CheckFirstThenMinusOne with every pointer present before initialization"); + Require(Hmd2CheckFirstThenMinusOne(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenMinusOne with pointer 0 null before initialization"); + Require(Hmd2CheckFirstTwoThenParameterInvalid(dummy, dummy) == Hmd2::ParameterInvalid, "Hmd2CheckFirstTwoThenParameterInvalid with every pointer present before initialization"); + Require(Hmd2CheckFirstTwoThenParameterInvalid(nullptr, dummy) == Hmd2::ParameterNull, "Hmd2CheckFirstTwoThenParameterInvalid with pointer 0 null before initialization"); + Require(Hmd2CheckFirstTwoThenParameterInvalid(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstTwoThenParameterInvalid with pointer 1 null before initialization"); + Require(Hmd2CheckSecondThenError002C(dummy, dummy) == Hmd2::Error002C, "Hmd2CheckSecondThenError002C with every pointer present before initialization"); + Require(Hmd2CheckSecondThenError002C(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckSecondThenError002C with pointer 1 null before initialization"); + Require(Hmd2CheckFirstThenParameterInvalid(dummy) == Hmd2::ParameterInvalid, "Hmd2CheckFirstThenParameterInvalid with every pointer present before initialization"); + Require(Hmd2CheckFirstThenParameterInvalid(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenParameterInvalid with pointer 0 null before initialization"); + Require(Hmd2CheckAllSixThenZero(dummy, dummy, dummy, dummy, dummy, dummy) == 0, "Hmd2CheckAllSixThenZero with every pointer present before initialization"); + Require(Hmd2CheckAllSixThenZero(nullptr, dummy, dummy, dummy, dummy, dummy) == Hmd2::ParameterNull, "Hmd2CheckAllSixThenZero with pointer 0 null before initialization"); + Require(Hmd2CheckAllSixThenZero(dummy, nullptr, dummy, dummy, dummy, dummy) == Hmd2::ParameterNull, "Hmd2CheckAllSixThenZero with pointer 1 null before initialization"); + Require(Hmd2CheckAllSixThenZero(dummy, dummy, nullptr, dummy, dummy, dummy) == Hmd2::ParameterNull, "Hmd2CheckAllSixThenZero with pointer 2 null before initialization"); + Require(Hmd2CheckAllSixThenZero(dummy, dummy, dummy, nullptr, dummy, dummy) == Hmd2::ParameterNull, "Hmd2CheckAllSixThenZero with pointer 3 null before initialization"); + Require(Hmd2CheckAllSixThenZero(dummy, dummy, dummy, dummy, nullptr, dummy) == Hmd2::ParameterNull, "Hmd2CheckAllSixThenZero with pointer 4 null before initialization"); + Require(Hmd2CheckAllSixThenZero(dummy, dummy, dummy, dummy, dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckAllSixThenZero with pointer 5 null before initialization"); + Require(Hmd2CheckSecondThenParameterInvalid(dummy, dummy) == Hmd2::ParameterInvalid, "Hmd2CheckSecondThenParameterInvalid with every pointer present before initialization"); + Require(Hmd2CheckSecondThenParameterInvalid(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckSecondThenParameterInvalid with pointer 1 null before initialization"); + Require(Hmd2CheckFirstThenError800d0203(dummy) == Hmd2::Error800d0203, "Hmd2CheckFirstThenError800d0203 with every pointer present before initialization"); + Require(Hmd2CheckFirstThenError800d0203(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenError800d0203 with pointer 0 null before initialization"); + Require(Hmd2CheckFirstThenError1003(dummy) == Hmd2::Error1003, "Hmd2CheckFirstThenError1003 with every pointer present before initialization"); + Require(Hmd2CheckFirstThenError1003(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenError1003 with pointer 0 null before initialization"); + Require(Hmd2CheckFirstThenError1010(dummy) == Hmd2::Error1010, "Hmd2CheckFirstThenError1010 with every pointer present before initialization"); + Require(Hmd2CheckFirstThenError1010(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenError1010 with pointer 0 null before initialization"); + Require(Hmd2CheckFirstTwoThenError1002(dummy, dummy) == Hmd2::Error1002, "Hmd2CheckFirstTwoThenError1002 with every pointer present before initialization"); + Require(Hmd2CheckFirstTwoThenError1002(nullptr, dummy) == Hmd2::ParameterNull, "Hmd2CheckFirstTwoThenError1002 with pointer 0 null before initialization"); + Require(Hmd2CheckFirstTwoThenError1002(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstTwoThenError1002 with pointer 1 null before initialization"); + Require(Hmd2CheckFirstTwoThenError1003(dummy, dummy) == Hmd2::Error1003, "Hmd2CheckFirstTwoThenError1003 with every pointer present before initialization"); + Require(Hmd2CheckFirstTwoThenError1003(nullptr, dummy) == Hmd2::ParameterNull, "Hmd2CheckFirstTwoThenError1003 with pointer 0 null before initialization"); + Require(Hmd2CheckFirstTwoThenError1003(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstTwoThenError1003 with pointer 1 null before initialization"); + Require(Hmd2CheckFirstAndThirdThenZero(dummy, dummy, dummy) == 0, "Hmd2CheckFirstAndThirdThenZero with every pointer present before initialization"); + Require(Hmd2CheckFirstAndThirdThenZero(nullptr, dummy, dummy) == Hmd2::ParameterNull, "Hmd2CheckFirstAndThirdThenZero with pointer 0 null before initialization"); + Require(Hmd2CheckFirstAndThirdThenZero(dummy, dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstAndThirdThenZero with pointer 2 null before initialization"); + Require(Hmd2CheckFirstAndThirdThenError1003(dummy, dummy, dummy) == Hmd2::Error1003, "Hmd2CheckFirstAndThirdThenError1003 with every pointer present before initialization"); + Require(Hmd2CheckFirstAndThirdThenError1003(nullptr, dummy, dummy) == Hmd2::ParameterNull, "Hmd2CheckFirstAndThirdThenError1003 with pointer 0 null before initialization"); + Require(Hmd2CheckFirstAndThirdThenError1003(dummy, dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstAndThirdThenError1003 with pointer 2 null before initialization"); + Require(Hmd2CheckSecondThenInvalidHandle(dummy, dummy) == Hmd2::InvalidHandle, "Hmd2CheckSecondThenInvalidHandle with every pointer present before initialization"); + Require(Hmd2CheckSecondThenInvalidHandle(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckSecondThenInvalidHandle with pointer 1 null before initialization"); + Require(Hmd2CheckSecondThenError1002(dummy, dummy) == Hmd2::Error1002, "Hmd2CheckSecondThenError1002 with every pointer present before initialization"); + Require(Hmd2CheckSecondThenError1002(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckSecondThenError1002 with pointer 1 null before initialization"); + Require(Hmd2BeforeInitializeZero() == 0, "Hmd2BeforeInitializeZero before initialization"); + Require(Hmd2BeforeInitializeNotInitialized() == Hmd2::NotInitialized, "Hmd2BeforeInitializeNotInitialized before initialization"); + Require(Hmd2BeforeInitializeError0004() == Hmd2::Error0004, "Hmd2BeforeInitializeError0004 before initialization"); + Require(Hmd2BeforeInitializeParameterNull() == Hmd2::ParameterNull, "Hmd2BeforeInitializeParameterNull before initialization"); + Require(Hmd2BeforeInitializeParameterInvalid() == Hmd2::ParameterInvalid, "Hmd2BeforeInitializeParameterInvalid before initialization"); + Require(Hmd2BeforeInitializeReprojectionNotInitialized() == Hmd2::ReprojectionNotInitialized, "Hmd2BeforeInitializeReprojectionNotInitialized before initialization"); + Require(Hmd2BeforeInitializeError002C() == Hmd2::Error002C, "Hmd2BeforeInitializeError002C before initialization"); + Require(Hmd2BeforeInitializeError1002() == Hmd2::Error1002, "Hmd2BeforeInitializeError1002 before initialization"); + Require(Hmd2BeforeInitializeError1003() == Hmd2::Error1003, "Hmd2BeforeInitializeError1003 before initialization"); + Require(Hmd2BeforeInitializeMinusOne() == -1, "Hmd2BeforeInitializeMinusOne before initialization"); + Require(Hmd2BeforeInitializeValue0400() == 0x400, "Hmd2BeforeInitializeValue0400 before initialization"); + Require(Hmd2BeforeInitializeValue20000() == 0x20000, "Hmd2BeforeInitializeValue20000 before initialization"); Require(sceHmd2SetVibration(0, 0) == 0, "vibration of the unopened handle 0 failed"); } @@ -59,35 +190,115 @@ void TestWithoutHeadset() { Hmd2::DeviceInformation info; Require(sceHmd2GetDeviceInformation(&info) == Hmd2::NotInitialized, "device information without a headset"); Require(sceHmd2Terminate() == Hmd2::NotInitialized, "terminate after the failed initialization"); + { + std::uint8_t out[64]; + std::uint64_t words[2]; + Require(Hmd2WriteStateTwo(nullptr) == Hmd2::ParameterNull, "Hmd2WriteStateTwo null after the failed initialization"); + std::memset(out, 0xaa, sizeof out); + Require(Hmd2WriteStateTwo(out) == 0 && out[0] == 2 && out[1] == 0 && out[4] == 0 && out[5] == 0xaa, "Hmd2WriteStateTwo bytes after the failed initialization"); + Require(Hmd2WriteSize0x2000Align0x100(nullptr, words) == Hmd2::ParameterNull && Hmd2WriteSize0x2000Align0x100(words, nullptr) == Hmd2::ParameterNull, "Hmd2WriteSize0x2000Align0x100 null after the failed initialization"); + words[0] = words[1] = ~0ull; + Require(Hmd2WriteSize0x2000Align0x100(words, words + 1) == 0 && words[0] == 0x2000 && words[1] == 0x100, "Hmd2WriteSize0x2000Align0x100 values after the failed initialization"); + Require(Hmd2WriteFourConstants(nullptr) == Hmd2::ParameterNull, "Hmd2WriteFourConstants null after the failed initialization"); + std::memset(out, 0xaa, sizeof out); + Require(Hmd2WriteFourConstants(out) == 0 && out[0] == 0xe1 && out[15] == 0x3c && out[16] == 0xaa, "Hmd2WriteFourConstants bytes after the failed initialization"); + Require(Hmd2WriteSixHalvesThenInvalidHandle(0, nullptr) == Hmd2::ParameterNull, "Hmd2WriteSixHalvesThenInvalidHandle null after the failed initialization"); + std::memset(out, 0xaa, sizeof out); + Require(Hmd2WriteSixHalvesThenInvalidHandle(0, out) == Hmd2::InvalidHandle && out[7] == 0x3f && out[20] == 0x00 && out[23] == 0x3f && out[24] == 0xaa, "Hmd2WriteSixHalvesThenInvalidHandle bytes after the failed initialization"); + Require(Hmd2WriteTwelveHalvesThenInvalidHandle(0, nullptr) == Hmd2::ParameterNull, "Hmd2WriteTwelveHalvesThenInvalidHandle null after the failed initialization"); + std::memset(out, 0xaa, sizeof out); + Require(Hmd2WriteTwelveHalvesThenInvalidHandle(0, out) == Hmd2::InvalidHandle && out[7] == 0x3f && out[31] == 0x3f && out[47] == 0x00 && out[48] == 0xaa, "Hmd2WriteTwelveHalvesThenInvalidHandle bytes after the failed initialization"); + Require(Hmd2SlotUpTo3CheckSecondThirdThenZero(9, nullptr, out) == Hmd2::ParameterNull && Hmd2SlotUpTo3CheckSecondThirdThenZero(0, out, nullptr) == Hmd2::ParameterNull, "Hmd2SlotUpTo3CheckSecondThirdThenZero null after the failed initialization"); + Require(Hmd2SlotUpTo3CheckSecondThirdThenZero(3, out, out) == 0 && Hmd2SlotUpTo3CheckSecondThirdThenZero(4, out, out) == Hmd2::ParameterInvalid && Hmd2SlotUpTo3CheckSecondThirdThenZero(~0ull, out, out) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo3CheckSecondThirdThenZero slots after the failed initialization"); + Require(Hmd2SlotUpTo1ThenError1002(1) == Hmd2::Error1002 && Hmd2SlotUpTo1ThenError1002(2) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo1ThenError1002 after the failed initialization"); + Require(Hmd2SlotUpTo1CheckSecondThenError1002(5, nullptr) == Hmd2::ParameterNull && Hmd2SlotUpTo1CheckSecondThenError1002(0, out) == Hmd2::Error1002 && Hmd2SlotUpTo1CheckSecondThenError1002(2, out) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo1CheckSecondThenError1002 after the failed initialization"); + Require(Hmd2SlotUpTo1ThenMinusOne(1) == -1 && Hmd2SlotUpTo1ThenMinusOne(2) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo1ThenMinusOne after the failed initialization"); + Require(Hmd2SlotUpTo2ThenMinusOne(2) == -1 && Hmd2SlotUpTo2ThenMinusOne(3) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo2ThenMinusOne after the failed initialization"); + Require(Hmd2SlotUpTo2ThenError1002(2) == Hmd2::Error1002 && Hmd2SlotUpTo2ThenError1002(3) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo2ThenError1002 after the failed initialization"); + Require(Hmd2SlotUpTo2ThenError1003(0) == Hmd2::Error1003 && Hmd2SlotUpTo2ThenError1003(16) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo2ThenError1003 after the failed initialization"); + Require(Hmd2SlotUpTo2CheckSecondThenError002C(8, nullptr) == Hmd2::ParameterNull && Hmd2SlotUpTo2CheckSecondThenError002C(2, out) == Hmd2::Error002C && Hmd2SlotUpTo2CheckSecondThenError002C(3, out) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo2CheckSecondThenError002C after the failed initialization"); + Require(Hmd2IndexZeroCheckSecondThenZero(1, nullptr) == Hmd2::ParameterNull && Hmd2IndexZeroCheckSecondThenZero(0, out) == 0 && Hmd2IndexZeroCheckSecondThenZero(1, out) == Hmd2::Error100F, "Hmd2IndexZeroCheckSecondThenZero after the failed initialization"); + } + const std::uint8_t dummyBytes[16]{}; + const void* dummy = dummyBytes; + Require(Hmd2CheckFirstThenError0004(dummy) == Hmd2::Error0004, "Hmd2CheckFirstThenError0004 with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstThenError0004(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenError0004 with pointer 0 null after the failed initialization"); + Require(Hmd2CheckFirstThenZero(dummy) == 0, "Hmd2CheckFirstThenZero with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstThenZero(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenZero with pointer 0 null after the failed initialization"); + Require(Hmd2CheckFirstThenError1002(dummy) == Hmd2::Error1002, "Hmd2CheckFirstThenError1002 with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstThenError1002(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenError1002 with pointer 0 null after the failed initialization"); + Require(Hmd2CheckFirstThenMinusOne(dummy) == -1, "Hmd2CheckFirstThenMinusOne with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstThenMinusOne(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenMinusOne with pointer 0 null after the failed initialization"); + Require(Hmd2CheckFirstTwoThenParameterInvalid(dummy, dummy) == Hmd2::ParameterInvalid, "Hmd2CheckFirstTwoThenParameterInvalid with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstTwoThenParameterInvalid(nullptr, dummy) == Hmd2::ParameterNull, "Hmd2CheckFirstTwoThenParameterInvalid with pointer 0 null after the failed initialization"); + Require(Hmd2CheckFirstTwoThenParameterInvalid(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstTwoThenParameterInvalid with pointer 1 null after the failed initialization"); + Require(Hmd2CheckSecondThenError002C(dummy, dummy) == Hmd2::Error002C, "Hmd2CheckSecondThenError002C with every pointer present after the failed initialization"); + Require(Hmd2CheckSecondThenError002C(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckSecondThenError002C with pointer 1 null after the failed initialization"); + Require(Hmd2CheckFirstThenParameterInvalid(dummy) == Hmd2::ParameterInvalid, "Hmd2CheckFirstThenParameterInvalid with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstThenParameterInvalid(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenParameterInvalid with pointer 0 null after the failed initialization"); + Require(Hmd2CheckAllSixThenZero(dummy, dummy, dummy, dummy, dummy, dummy) == 0, "Hmd2CheckAllSixThenZero with every pointer present after the failed initialization"); + Require(Hmd2CheckAllSixThenZero(nullptr, dummy, dummy, dummy, dummy, dummy) == Hmd2::ParameterNull, "Hmd2CheckAllSixThenZero with pointer 0 null after the failed initialization"); + Require(Hmd2CheckAllSixThenZero(dummy, nullptr, dummy, dummy, dummy, dummy) == Hmd2::ParameterNull, "Hmd2CheckAllSixThenZero with pointer 1 null after the failed initialization"); + Require(Hmd2CheckAllSixThenZero(dummy, dummy, nullptr, dummy, dummy, dummy) == Hmd2::ParameterNull, "Hmd2CheckAllSixThenZero with pointer 2 null after the failed initialization"); + Require(Hmd2CheckAllSixThenZero(dummy, dummy, dummy, nullptr, dummy, dummy) == Hmd2::ParameterNull, "Hmd2CheckAllSixThenZero with pointer 3 null after the failed initialization"); + Require(Hmd2CheckAllSixThenZero(dummy, dummy, dummy, dummy, nullptr, dummy) == Hmd2::ParameterNull, "Hmd2CheckAllSixThenZero with pointer 4 null after the failed initialization"); + Require(Hmd2CheckAllSixThenZero(dummy, dummy, dummy, dummy, dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckAllSixThenZero with pointer 5 null after the failed initialization"); + Require(Hmd2CheckSecondThenParameterInvalid(dummy, dummy) == Hmd2::ParameterInvalid, "Hmd2CheckSecondThenParameterInvalid with every pointer present after the failed initialization"); + Require(Hmd2CheckSecondThenParameterInvalid(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckSecondThenParameterInvalid with pointer 1 null after the failed initialization"); + Require(Hmd2CheckFirstThenError800d0203(dummy) == Hmd2::Error800d0203, "Hmd2CheckFirstThenError800d0203 with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstThenError800d0203(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenError800d0203 with pointer 0 null after the failed initialization"); + Require(Hmd2CheckFirstThenError1003(dummy) == Hmd2::Error1003, "Hmd2CheckFirstThenError1003 with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstThenError1003(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenError1003 with pointer 0 null after the failed initialization"); + Require(Hmd2CheckFirstThenError1010(dummy) == Hmd2::Error1010, "Hmd2CheckFirstThenError1010 with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstThenError1010(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenError1010 with pointer 0 null after the failed initialization"); + Require(Hmd2CheckFirstTwoThenError1002(dummy, dummy) == Hmd2::Error1002, "Hmd2CheckFirstTwoThenError1002 with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstTwoThenError1002(nullptr, dummy) == Hmd2::ParameterNull, "Hmd2CheckFirstTwoThenError1002 with pointer 0 null after the failed initialization"); + Require(Hmd2CheckFirstTwoThenError1002(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstTwoThenError1002 with pointer 1 null after the failed initialization"); + Require(Hmd2CheckFirstTwoThenError1003(dummy, dummy) == Hmd2::Error1003, "Hmd2CheckFirstTwoThenError1003 with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstTwoThenError1003(nullptr, dummy) == Hmd2::ParameterNull, "Hmd2CheckFirstTwoThenError1003 with pointer 0 null after the failed initialization"); + Require(Hmd2CheckFirstTwoThenError1003(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstTwoThenError1003 with pointer 1 null after the failed initialization"); + Require(Hmd2CheckFirstAndThirdThenZero(dummy, dummy, dummy) == 0, "Hmd2CheckFirstAndThirdThenZero with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstAndThirdThenZero(nullptr, dummy, dummy) == Hmd2::ParameterNull, "Hmd2CheckFirstAndThirdThenZero with pointer 0 null after the failed initialization"); + Require(Hmd2CheckFirstAndThirdThenZero(dummy, dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstAndThirdThenZero with pointer 2 null after the failed initialization"); + Require(Hmd2CheckFirstAndThirdThenError1003(dummy, dummy, dummy) == Hmd2::Error1003, "Hmd2CheckFirstAndThirdThenError1003 with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstAndThirdThenError1003(nullptr, dummy, dummy) == Hmd2::ParameterNull, "Hmd2CheckFirstAndThirdThenError1003 with pointer 0 null after the failed initialization"); + Require(Hmd2CheckFirstAndThirdThenError1003(dummy, dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstAndThirdThenError1003 with pointer 2 null after the failed initialization"); + Require(Hmd2CheckSecondThenInvalidHandle(dummy, dummy) == Hmd2::InvalidHandle, "Hmd2CheckSecondThenInvalidHandle with every pointer present after the failed initialization"); + Require(Hmd2CheckSecondThenInvalidHandle(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckSecondThenInvalidHandle with pointer 1 null after the failed initialization"); + Require(Hmd2CheckSecondThenError1002(dummy, dummy) == Hmd2::Error1002, "Hmd2CheckSecondThenError1002 with every pointer present after the failed initialization"); + Require(Hmd2CheckSecondThenError1002(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckSecondThenError1002 with pointer 1 null after the failed initialization"); + Require(Hmd2BeforeInitializeZero() == 0, "Hmd2BeforeInitializeZero after the failed initialization"); + Require(Hmd2BeforeInitializeNotInitialized() == Hmd2::NotInitialized, "Hmd2BeforeInitializeNotInitialized after the failed initialization"); + Require(Hmd2BeforeInitializeError0004() == Hmd2::Error0004, "Hmd2BeforeInitializeError0004 after the failed initialization"); + Require(Hmd2BeforeInitializeParameterNull() == Hmd2::ParameterNull, "Hmd2BeforeInitializeParameterNull after the failed initialization"); + Require(Hmd2BeforeInitializeParameterInvalid() == Hmd2::ParameterInvalid, "Hmd2BeforeInitializeParameterInvalid after the failed initialization"); + Require(Hmd2BeforeInitializeReprojectionNotInitialized() == Hmd2::ReprojectionNotInitialized, "Hmd2BeforeInitializeReprojectionNotInitialized after the failed initialization"); + Require(Hmd2BeforeInitializeError002C() == Hmd2::Error002C, "Hmd2BeforeInitializeError002C after the failed initialization"); + Require(Hmd2BeforeInitializeError1002() == Hmd2::Error1002, "Hmd2BeforeInitializeError1002 after the failed initialization"); + Require(Hmd2BeforeInitializeError1003() == Hmd2::Error1003, "Hmd2BeforeInitializeError1003 after the failed initialization"); + Require(Hmd2BeforeInitializeMinusOne() == -1, "Hmd2BeforeInitializeMinusOne after the failed initialization"); + Require(Hmd2BeforeInitializeValue0400() == 0x400, "Hmd2BeforeInitializeValue0400 after the failed initialization"); + Require(Hmd2BeforeInitializeValue20000() == 0x20000, "Hmd2BeforeInitializeValue20000 after the failed initialization"); } } +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(); - bool threw = false; - try { - sceHmd2GazeActivateGazeTracking(); - } catch (const std::runtime_error&) { - threw = true; - } - Require(threw, "gaze activation did not throw"); - threw = false; - try { - sceHmd2ReprojectionGetCurrentOutput(); - } catch (const std::runtime_error&) { - threw = true; - } - Require(threw, "reprojection get current output did not throw"); - threw = false; - try { - sceHmd2SetFieldOfView(); - } catch (const std::runtime_error&) { - threw = true; - } - Require(threw, "set field of view did not throw"); std::puts("HMD2 tests passed"); return 0; } diff --git a/core/libs/tests/GuestHostStandardError.cpp b/core/libs/tests/GuestHostStandardError.cpp index bf4df157ce..4c3fdc2309 100644 --- a/core/libs/tests/GuestHostStandardError.cpp +++ b/core/libs/tests/GuestHostStandardError.cpp @@ -23,7 +23,13 @@ int APS5_VABI fflush_nid_postfix(FileStream*); int APS5_VABI pipe_nid_postfix(int*); int APS5_VABI dup2_nid_postfix(int, int); std::int64_t APS5_VABI write_nid_postfix(int, const void*, std::size_t); +int APS5_VABI printf_nid_postfix(const char*, ...); +int APS5_VABI puts_nid_postfix(const char*); +int APS5_VABI fprintf_nid_postfix(FileStream*, const char*, ...); +void APS5_VABI perror_nid_postfix(const char*); +int* APS5_VABI __error_nid_postfix(); extern FileStream* __stderrp_nid_postfix; +extern FileStream* __stdoutp_nid_postfix; } static void Require(bool value) { if (!value) std::abort(); } @@ -101,5 +107,68 @@ int main() { if (redirect == Redirect::Reopen) Require(Contents(probe).find("report kept") == std::string::npos); } std::filesystem::remove(probe); + + int host[2] = {-1, -1}; + Require(::pipe(host) == 0); + const pid_t child = ::fork(); + Require(child >= 0); + if (child == 0) { + ::close(host[0]); + if (::dup2(host[1], 1) != 1) ::_exit(70); + ::close(host[1]); + int guest[2] = {-1, -1}; + if (pipe_nid_postfix(guest) != 0 || dup2_nid_postfix(guest[1], 1) != 1) ::_exit(72); + std::fputs("host\n", stdout); + std::fflush(stdout); + ::_exit(0); + } + ::close(host[1]); + std::string output; + char buffer[64]; + for (ssize_t count; (count = ::read(host[0], buffer, sizeof(buffer))) > 0;) output.append(buffer, static_cast(count)); + ::close(host[0]); + int status = 0; + Require(::waitpid(child, &status, 0) == child); + Require(WIFEXITED(status) && WEXITSTATUS(status) == 0); + Require(output == "host\n"); + + for (const int standard : {1, 2}) { + int host[2] = {-1, -1}; + Require(::pipe(host) == 0); + const pid_t child = ::fork(); + Require(child >= 0); + if (child == 0) { + ::close(host[0]); + int guest[2] = {-1, -1}; + if (pipe_nid_postfix(guest) != 0 || dup2_nid_postfix(guest[1], standard) != standard) ::_exit(72); + const char* expected = "guest-fprintf\n"; + if (standard == 1) { + if (printf_nid_postfix("guest-printf\n") < 0 || puts_nid_postfix("guest-puts") < 0 || fflush_nid_postfix(__stdoutp_nid_postfix) != 0) ::_exit(73); + expected = "guest-printf\nguest-puts\n"; + } else { + if (fprintf_nid_postfix(__stderrp_nid_postfix, "guest-fprintf\n") < 0) ::_exit(73); + *__error_nid_postfix() = 2; + perror_nid_postfix("guest-perror"); + if (fflush_nid_postfix(__stderrp_nid_postfix) != 0) ::_exit(73); + expected = "guest-fprintf\nguest-perror: No such file or directory\n"; + } + std::string got; + char buffer[64]; + const auto length = std::char_traits::length(expected); + for (ssize_t count; got.size() < length && (count = ::read(guest[0], buffer, sizeof(buffer))) > 0;) got.append(buffer, static_cast(count)); + if (got != expected) ::_exit(75); + if (::write(host[1], "ok", 2) != 2) ::_exit(76); + ::_exit(0); + } + ::close(host[1]); + std::string marker; + char buffer[8]; + for (ssize_t count; (count = ::read(host[0], buffer, sizeof(buffer))) > 0;) marker.append(buffer, static_cast(count)); + ::close(host[0]); + int status = 0; + Require(::waitpid(child, &status, 0) == child); + Require(WIFEXITED(status) && WEXITSTATUS(status) == 0); + Require(marker == "ok"); + } return 0; } diff --git a/core/libs/tests/GuestHostStandardErrorWindows.cpp b/core/libs/tests/GuestHostStandardErrorWindows.cpp new file mode 100644 index 0000000000..f91c92d09e --- /dev/null +++ b/core/libs/tests/GuestHostStandardErrorWindows.cpp @@ -0,0 +1,105 @@ +#include "prx/libc/include/FileStream.hpp" +#include "prx/libc/include/HostStandardStreams.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#ifdef _WIN32 +#include +#include +#include +#endif + +extern "C" { +int APS5_VABI pipe_nid_postfix(int*); +int APS5_VABI dup2_nid_postfix(int, int); +std::int64_t APS5_VABI write_nid_postfix(int, const void*, std::size_t); +int APS5_VABI printf_nid_postfix(const char*, ...); +int APS5_VABI puts_nid_postfix(const char*); +int APS5_VABI fprintf_nid_postfix(FileStream*, const char*, ...); +int APS5_VABI fflush_nid_postfix(FileStream*); +void APS5_VABI perror_nid_postfix(const char*); +int* APS5_VABI __error_nid_postfix(); +extern FileStream _Stdout_nid_postfix; +extern FileStream _Stderr_nid_postfix; +} + +static void Require(bool value) { if (!value) std::abort(); } + +static DWORD Available(int descriptor, void* buffer, DWORD capacity, DWORD& read) { + const auto handle = reinterpret_cast(::_get_osfhandle(descriptor)); + DWORD available = 0; + Require(::PeekNamedPipe(handle, buffer, capacity, &read, &available, nullptr) != 0); + return available; +} + +static void Split(int standard) { + int capture[2]{-1, -1}; + Require(::_pipe(capture, 65536, _O_BINARY) == 0); + const int original = ::_dup(standard); + Require(original >= 0 && ::_dup2(capture[1], standard) == 0); + Require(std::fputs("host-before\n", standard == 1 ? stdout : stderr) >= 0); + + int guest[2]{-1, -1}; + Require(pipe_nid_postfix(guest) == 0); + Require(dup2_nid_postfix(guest[1], standard) == standard); + std::FILE* const stream = standard == 1 ? stdout : stderr; + + std::string host(8192, 'h'); + host.back() = '\n'; + Require(std::fputs(host.c_str(), stream) >= 0); + Require(std::fflush(stream) == 0); + host.insert(0, "host-before\n"); + Require(write_nid_postfix(standard, "guest", 5) == 5); + + char guestBytes[8]{}; + DWORD guestRead = 0; + Require(Available(guest[0], guestBytes, sizeof(guestBytes), guestRead) == 5); + Require(guestRead == 5 && std::string(guestBytes, 5) == "guest"); + char hostBytes[8]{}; + DWORD hostRead = 0; + Require(Available(capture[0], hostBytes, sizeof(hostBytes), hostRead) == host.size()); + Require(hostRead == sizeof(hostBytes) && std::string(hostBytes, hostRead) == host.substr(0, hostRead)); + + Require(::_dup2(original, standard) == 0); + ::_close(original); + ::_close(capture[0]); + ::_close(capture[1]); + ::_close(guest[0]); + ::_close(guest[1]); +} + +static void GuestText(int standard, const std::string& text) { + int guest[2]{-1, -1}; + Require(pipe_nid_postfix(guest) == 0); + const int original = ::_dup(standard); + Require(original >= 0); + Require(dup2_nid_postfix(guest[1], standard) == standard); + if (standard == 1) { + Require(printf_nid_postfix("guest-printf\n") >= 0); + Require(puts_nid_postfix("guest-puts") >= 0); + Require(fflush_nid_postfix(&_Stdout_nid_postfix) == 0); + } else { + Require(fprintf_nid_postfix(&_Stderr_nid_postfix, "guest-fprintf\n") >= 0); + *__error_nid_postfix() = 2; + perror_nid_postfix("guest-perror"); + Require(fflush_nid_postfix(&_Stderr_nid_postfix) == 0); + } + std::string got(text.size(), '\0'); + DWORD read = 0; + Require(Available(guest[0], got.data(), static_cast(got.size()), read) == text.size()); + Require(read == text.size() && got == text); + Require(::_dup2(original, standard) == 0); + ::_close(original); + ::_close(guest[0]); + ::_close(guest[1]); +} + +int main() { + Split(2); + Split(1); + GuestText(1, "guest-printf\nguest-puts\n"); + GuestText(2, "guest-fprintf\nguest-perror: No such file or directory\n"); + return 0; +} diff --git a/core/libs/tests/GuestHttp.cpp b/core/libs/tests/GuestHttp.cpp index bd3a9b01c9..93eb0119d3 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,34 +184,49 @@ 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"}, - {"%41%4a%4F%ff", "AJO\xFF"}, {"%2520", "%20"}, - {"%", "%"}, {"%1", "%1"}, {"%1g%gg%+1%-1", "%1g%gg%+1%-1"} + {"%41%4a%4F%ff", "AJO\xFF"}, {"%2520", "%20"} }; for (const auto& row : unescapeCases) { const std::size_t size = std::strlen(row[1]) + 1; Require(sceHttpUriUnescape(nullptr, &required, 0, row[0]) == 0 && required == size); std::memset(escaped, 'Z', sizeof(escaped)); - Require(sceHttpUriUnescape(escaped, &required, size - 1, row[0]) == outOfMemory && required == size); - for (char c : escaped) Require(c == 'Z'); + required = 77; + Require(sceHttpUriUnescape(escaped, &required, size - 1, row[0]) == outOfMemory && required == 77); + Require(std::string_view(escaped, size - 1) == row[1] && escaped[size - 1] == 'Z'); Require(sceHttpUriUnescape(escaped, nullptr, size, row[0]) == 0); Require(Equal(escaped, row[1]) && escaped[size] == 'Z'); } + for (const char* malformed : {"%", "%1", "%1g", "%gg", "%+1", "%-1", "% 1", "abc%", "%%41", "%4%41"}) { + std::memset(escaped, 'Z', sizeof(escaped)); + required = 77; + Require(sceHttpUriUnescape(escaped, &required, sizeof(escaped), malformed) == invalidUrl && required == 77 && escaped[0] == 'Z'); + Require(sceHttpUriUnescape(nullptr, &required, 0, malformed) == invalidUrl && required == 77); + } Require(sceHttpUriUnescape(escaped, &required, sizeof(escaped), "a%00b") == 0); Require(required == 4 && std::memcmp(escaped, "a\0b", 4) == 0); std::strcpy(escaped, "%41%2f%2520"); Require(sceHttpUriUnescape(escaped, &required, sizeof(escaped), escaped) == 0); Require(Equal(escaped, "A/%20") && required == 6); - Require(sceHttpUriUnescape(nullptr, nullptr, 0, "valid") == 0); + Require(sceHttpUriUnescape(nullptr, nullptr, 0, "valid") == invalidValue); required = 123; Require(sceHttpUriUnescape(escaped, &required, sizeof(escaped), nullptr) == invalidValue); Require(required == 123 && Equal(escaped, "A/%20")); @@ -437,9 +473,14 @@ int main() { Require(sceHttpUriSweepPath(sweptPath, "/a/b/../c", 6) == 0 && Equal(sweptPath, "/a/b/")); Require(sceHttpUriSweepPath(sweptPath, "/ab/c", 3) == 0 && Equal(sweptPath, "/a")); Require(sceHttpUriSweepPath(sweptPath, "/a/./b", 5) == 0 && Equal(sweptPath, "/a/.")); - Require(sceHttpUriSweepPath(sweptPath, "/a/b/../c", 8) == 0 && Equal(sweptPath, "/a/b/..")); - char sweptByte[2] = {'x', 'x'}; - Require(sceHttpUriSweepPath(sweptByte, "/", 1) == 0 && sweptByte[0] == '\0' && sweptByte[1] == 'x'); + Require(sceHttpUriSweepPath(sweptPath, "/a/b/../c", 8) == 0 && Equal(sweptPath, "/a/")); + Require(sceHttpUriSweepPath(sweptPath, "/a/b/../c", 5) == 0 && Equal(sweptPath, "/a/b")); + Require(sweep("/a/b/..", "/a/b/..")); + Require(sweep("/a/b/./.", "/a/b/.")); + char sweptByte[3] = {'x', 'x', 'x'}; + Require(sceHttpUriSweepPath(sweptByte, "/a", 1) == 0 && sweptByte[0] == '/' && sweptByte[1] == '\0' && sweptByte[2] == 'x'); + sweptByte[0] = 'x'; + Require(sceHttpUriSweepPath(sweptByte, "", 1) == 0 && sweptByte[0] == '\0'); { SceHttpUriElement source{}; diff --git a/core/libs/tests/GuestHttpResponseHeader.cpp b/core/libs/tests/GuestHttpResponseHeader.cpp index e02738dbc7..76bbc60b5d 100644 --- a/core/libs/tests/GuestHttpResponseHeader.cpp +++ b/core/libs/tests/GuestHttpResponseHeader.cpp @@ -55,7 +55,7 @@ static void CheckGuardPage() { constexpr std::string_view text = "X: abc\r\n\tdef\r\n"; char* header = pages + pageSize - text.size(); std::memcpy(header, text.data(), text.size()); - Check({header, text.size()}, "x", 3, "abc\r\n\tdef", static_cast(text.size())); + Check({header, text.size()}, "x", 3, "abc\r\n\tdef\r\n", static_cast(text.size())); Check({header, 6}, "x", 3, "abc", 6); const char* value = header; std::size_t length = 123; @@ -75,17 +75,25 @@ int main() { constexpr int invalidResponse = static_cast(0x80432060); constexpr int invalidValue = static_cast(0x804321FE); constexpr int notFound = static_cast(0x80432025); - Check("HTTP/1.1 200 OK\r\nX: \tvalue \t\r\nNext: no\r\n", "x", 21, "value \t", 30); - Check("X: first\r\nX: second\r\n", "X", 3, "first", 10); - Check("X: abc\n def\n\tghi\nY: no\n", "x", 3, "abc\n def\n\tghi", 17); + Check("HTTP/1.1 200 OK\r\nX: \tvalue \t\r\nNext: no\r\n", "x", 21, "value \t", 28); + Check("X: first\r\nX: second\r\n", "X", 3, "first", 8); + Check("X: abc\n def\n\tghi\nY: no\n", "x", 3, "abc\n def\n\tghi", 16); Check("X: no-newline", "X", 3, "no-newline", 13); Check("X: value\r", "X", 3, "value\r", 9); Check("X:", "X", 2, "", 0); Check("X: \t", "X", 4, "", 0); Check("X:\r\n\r\n", "X", 5, "", 0); - Check("X:\r\nnext\r\n", "X", 4, "next", 10); - Check("Prefix: wrong\n X: wrong\nXy: wrong\nX: right\n", "X", 37, "right", 43); - Check(std::string_view("X: a\0b\n", 7), "X", 3, std::string_view("a\0b", 3), 7); + Check("X:\r\nnext\r\n", "X", 0, "", 0); + Check("X:\n b\r\n", "X", 4, "b\r\n", 7); + Check("X:\r\n\tb\r\nY: c", "X", 5, "b", 6); + Check("Prefix: wrong\n X: wrong\nXy: wrong\nX: right\n", "X", 37, "right\n", 43); + Check("HTTP/1.1 200 OK\r\nServer: x\r\n\r\nBody: no\r\n", "Body", 36, "no\r\n", 40); + Check(" A: b\r\nB: c \r\n", "B", 11, "c \r\n", 16); + Check("Empty:\r\nSpaces: \r\nNoSpace:v\r\nZ: z", "Empty", 0, "", 0); + Check("Empty:\r\nSpaces: \r\nNoSpace:v\r\nZ: z", "Spaces", 0, "", 0); + Check("Empty:\r\nSpaces: \r\nNoSpace:v\r\nZ: z", "NoSpace", 29, "v", 30); + Check(std::string_view("X: a\0b\n", 7), "X", 3, std::string_view("a\0b\n", 4), 7); + Check(std::string_view("X: a\0b\r\nY: z", 13), "X", 3, std::string_view("a\0b", 3), 6); const char* marker = "unchanged"; const char* value = marker; @@ -104,6 +112,6 @@ int main() { std::string field(0xfff, 'A'); std::string header = field + ": value\n"; field += "ignored"; - Check(header, field.c_str(), 0x1000 + 1, "value", static_cast(header.size())); + Check(header, field.c_str(), 0x1000 + 1, "value\n", static_cast(header.size())); CheckGuardPage(); } diff --git a/core/libs/tests/GuestImeDialog.cpp b/core/libs/tests/GuestImeDialog.cpp index 3bfec70576..574398c192 100644 --- a/core/libs/tests/GuestImeDialog.cpp +++ b/core/libs/tests/GuestImeDialog.cpp @@ -13,6 +13,7 @@ int APS5_VABI sceImeDialogGetStatus(void); int APS5_VABI sceImeDialogGetResult(Result*); int APS5_VABI sceImeDialogAbort(void); int APS5_VABI sceImeDialogTerm(void); +int APS5_VABI sceImeDialogForceClose(void); } namespace { @@ -25,6 +26,7 @@ constexpr std::uint32_t EndStatusUserCanceled = 1; constexpr std::uint32_t EndStatusAborted = 2; constexpr int ErrorBusy = static_cast(0x80bc0001u); constexpr int ErrorNotOpened = static_cast(0x80bc0002u); +constexpr int ErrorNotInUse = static_cast(0x80bc0107u); constexpr int ErrorInvalidMaxTextLength = static_cast(0x80bc0016u); constexpr int ErrorInvalidInputTextBuffer = static_cast(0x80bc0017u); constexpr int ErrorInvalidAddress = static_cast(0x80bc0031u); @@ -146,6 +148,35 @@ int main() { Require(std::u16string(buffer) == u"keep"); Close(); + Require(sceImeDialogForceClose() == ErrorNotInUse); + Open(param); + Type("zz"); + Require(sceImeDialogForceClose() == 0); + Require(sceImeDialogGetStatus() == StatusNone); + Require(!ImeCapturesKeyboard_nid_postfix()); + Type("ignored"); + Press(SDL_SCANCODE_RETURN); + Require(std::u16string(buffer) == u"keep"); + Result closed{}; + Require(sceImeDialogGetResult(&closed) == ErrorNotOpened); + Require(sceImeDialogTerm() == ErrorNotOpened); + Require(sceImeDialogForceClose() == ErrorNotInUse); + + Open(param); + Type("!"); + Press(SDL_SCANCODE_RETURN); + Require(sceImeDialogGetStatus() == StatusFinished); + Require(sceImeDialogForceClose() == 0); + Require(sceImeDialogGetStatus() == StatusNone); + Require(std::u16string(buffer) == u"keep!"); + + Open(param); + Require(sceImeDialogAbort() == 0); + Require(sceImeDialogForceClose() == 0); + Require(sceImeDialogGetStatus() == StatusNone); + Open(param); + Close(); + Require(sceImeDialogInit(nullptr, nullptr) == ErrorInvalidAddress); const Param empty = DialogParam(buffer, 0); Require(sceImeDialogInit(&empty, nullptr) == ErrorInvalidMaxTextLength); diff --git a/core/libs/tests/GuestImeFilterText.cpp b/core/libs/tests/GuestImeFilterText.cpp new file mode 100644 index 0000000000..bac9e2daa2 --- /dev/null +++ b/core/libs/tests/GuestImeFilterText.cpp @@ -0,0 +1,108 @@ +#include "SceTypes.hpp" +#include "prx/libSceIme/include/ImeFilterText.hpp" +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceImeFilterText(char16_t* dst, int32_t* out_length, uint32_t capacity, + const ime_filter::FilterParam* param, const char16_t* src, uint32_t src_length); +} + +namespace { + +constexpr int kInvalidParam = static_cast(0x80bc0030u); +constexpr int kInvalidAddress = static_cast(0x80bc0031u); + +void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +struct Result { + int code; + std::vector text; +}; + +Result Run(uint64_t mask, uint32_t type, uint32_t capacity, std::initializer_list input, + uint32_t length = UINT32_MAX) { + std::vector src(input); + src.push_back(0); + if (length == UINT32_MAX) length = static_cast(input.size()); + std::vector dst(capacity + 4, 0xcccc); + int32_t count = -1; + const ime_filter::FilterParam param{mask, type, 0, 0}; + const int code = sceImeFilterText(dst.data(), &count, capacity, ¶m, src.data(), length); + Result result{code, {}}; + if (code == 0) { + Require(count >= 0 && static_cast(count) < capacity, "length must leave room for the terminator"); + Require(dst[static_cast(count)] == 0, "output must be terminated"); + result.text.assign(dst.begin(), dst.begin() + count); + } + return result; +} + +void Expect(const Result& result, std::initializer_list expected, const char* message) { + Require(result.code == 0, message); + Require(result.text == std::vector(expected), message); +} + +void CheckArguments() { + char16_t dst[4]; + int32_t count = 0; + const char16_t src[2] = {u'a', 0}; + const ime_filter::FilterParam param{0, 1, 0, 0}; + Require(sceImeFilterText(nullptr, &count, 4, ¶m, src, 1) == kInvalidAddress, "null destination"); + Require(sceImeFilterText(dst, nullptr, 4, ¶m, src, 1) == kInvalidAddress, "null length"); + Require(sceImeFilterText(dst, &count, 4, nullptr, src, 1) == kInvalidAddress, "null parameter"); + Require(sceImeFilterText(dst, &count, 4, ¶m, nullptr, 1) == kInvalidAddress, "null source"); + Require(Run(0, 1, 0, {u'a'}).code == kInvalidParam, "capacity 0"); + Require(Run(0, 1, 1, {u'a'}).code == kInvalidParam, "capacity below 2"); + Require(Run(0, 5, 16, {u'a'}).code == kInvalidParam, "type 5"); + Require(Run(0, 0xff, 16, {u'a'}).code == kInvalidParam, "type 0xff"); + Require(Run(0, 0x104, 16, {u'a'}).code == kInvalidParam, "type 0x104"); +} + +void CheckSimpleTypes() { + Expect(Run(0, 1, 16, {0x41, 0x7f, 0x42, 0xe9, 0x20ac, 0x7e}), {0x41, 0x42, 0x7e}, "ascii type"); + Expect(Run(0, 3, 16, {0x41, 0x7f, 0x42}), {0x41, 0x42}, "type 3"); + Expect(Run(0, 0x102, 16, {0x41, 0xe9, 0x7e}), {0x41, 0x7e}, "type 0x102"); + Expect(Run(0, 4, 16, {0x31, 0x2c, 0x41, 0x2e, 0x2d, 0x0a, 0xff11}), {0x31, 0x2c, 0x2e, 0x2d, 0x0a}, "number type"); +} + +void CheckLatinType() { + Expect(Run(0, 0x100, 16, {0x41, 0x17e, 0x17f, 0x1a0, 0x218}), {0x41, 0x17e, 0x1a0, 0x218}, "latin type"); + Expect(Run(0x4000001f, 0x100, 16, {0x41, 0x17f, 0x1a0, 0x1af, 0x20ab, 0x300, 0x323, 0x1ea0, 0x218}), + {0x41, 0x1a0, 0x1af, 0x20ab, 0x300, 0x323, 0x1ea0}, "vietnamese latin"); + Expect(Run(0x8000001f, 0x100, 16, {0x41, 0x218, 0x219, 0x21a, 0x21b, 0x21c}), + {0x41, 0x218, 0x219, 0x21a, 0x21b}, "romanian latin"); +} + +void CheckClassifiedType() { + Expect(Run(0, 0, 32, {0x31, 0x2c, 0x42, 0x39, 0xe9, 0x3b1, 0x430, 0x4e2d, 0x3042, 0xe01, 0x627, 0xac00}), + {0x31, 0x2c, 0x42, 0x39, 0xe9, 0x3b1, 0x430, 0x4e2d, 0x3042, 0xe01, 0x627, 0xac00}, "default classes"); + Expect(Run(0x400001f, 0, 16, {0x41, 0xe01, 0xe5b, 0xe80, 0x627}), {0x41, 0xe01, 0xe5b}, "thai class"); + Expect(Run(0x20000041full, 0, 16, {0x41, 0x410, 0x411}), {0x41, 0x410, 0x411}, "cyrillic class"); +} + +void CheckBounds() { + Expect(Run(0, 1, 4, {0x41, 0x42, 0x43, 0x44, 0x45}), {0x41, 0x42, 0x43}, "capacity limit"); + Expect(Run(0, 1, 16, {0x41, 0, 0x42}), {0x41}, "stops at terminator"); + Expect(Run(0, 1, 16, {0x41, 0x42, 0x43}, 2), {0x41, 0x42}, "source length limit"); + Expect(Run(0, 1, 16, {0x41, 0x42, 0x43}, 0), {}, "empty source"); +} + +} + +int main() { + CheckArguments(); + CheckSimpleTypes(); + CheckLatinType(); + CheckClassifiedType(); + CheckBounds(); + return 0; +} 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/GuestIndex.cpp b/core/libs/tests/GuestIndex.cpp new file mode 100644 index 0000000000..78fa2b8672 --- /dev/null +++ b/core/libs/tests/GuestIndex.cpp @@ -0,0 +1,97 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#include +#include +#endif + +namespace { + +using Search = char* (APS5_VABI*)(const char*, int); + +void Require(bool value, const char* message) { + if (!value) { + std::fprintf(stderr, "%s\n", message); + std::abort(); + } +} + +char* Expected(const char* text, int character) { + const char wanted = static_cast(character); + for (;;) { + if (*text == wanted) return const_cast(text); + if (*text == '\0') return nullptr; + ++text; + } +} + +void Check(Search search, const char* text) { + for (int character = -512; character <= 512; ++character) { + Require(search(text, character) == Expected(text, character), "index returned the wrong position"); + } + for (int character : {INT_MIN, INT_MAX}) { + Require(search(text, character) == Expected(text, character), "index truncated an int incorrectly"); + } +} + +} + +int main(int argc, char** argv) { + Require(argc == 2, "expected the library path"); +#ifdef _WIN32 + const auto library = LoadLibraryA(argv[1]); + Require(library != nullptr, "could not load libc"); + const auto search = reinterpret_cast(GetProcAddress(library, "index_nid_postfix")); + SYSTEM_INFO info{}; + GetSystemInfo(&info); + const std::size_t page = info.dwPageSize; + auto* memory = static_cast(VirtualAlloc(nullptr, 2 * page, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + Require(memory != nullptr, "could not allocate the boundary fixture"); + DWORD previous = 0; + Require(VirtualProtect(memory + page, page, PAGE_NOACCESS, &previous) != 0, "could not protect the boundary fixture"); +#else + const auto library = dlopen(argv[1], RTLD_NOW | RTLD_LOCAL); + Require(library != nullptr, "could not load libc"); + const auto search = reinterpret_cast(dlsym(library, "index_nid_postfix")); + const long pageSize = sysconf(_SC_PAGESIZE); + Require(pageSize > 0, "could not query page size"); + const auto page = static_cast(pageSize); + auto* memory = static_cast(mmap(nullptr, 2 * page, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); + Require(memory != MAP_FAILED, "could not allocate the boundary fixture"); + Require(mprotect(memory + page, page, PROT_NONE) == 0, "could not protect the boundary fixture"); +#endif + Require(search != nullptr, "index is missing from libc"); + Check(search, ""); + Check(search, "banana"); + const std::array bytes{'a', static_cast(0x80), 'a', static_cast(0xff), '\0', 'z', 'a', '\0', '\0'}; + const auto before = bytes; + Check(search, bytes.data()); + Require(bytes == before, "index modified its input"); + for (std::size_t length = 0; length <= 256; ++length) { + char* text = memory + page - length - 1; + for (std::size_t i = 0; i < length; ++i) text[i] = static_cast(1 + i % 255); + text[length] = '\0'; + Check(search, text); + } + char writable[] = "xyx"; + char* found = search(writable, 'y'); + Require(found == writable + 1, "index did not return the caller's storage"); + *found = 'z'; + Require(std::strcmp(writable, "xzx") == 0, "index returned an unwritable copy"); +#ifdef _WIN32 + Require(VirtualFree(memory, 0, MEM_RELEASE) != 0, "could not free the boundary fixture"); + FreeLibrary(library); +#else + Require(munmap(memory, 2 * page) == 0, "could not free the boundary fixture"); + dlclose(library); +#endif + return 0; +} 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..018dba662d 100644 --- a/core/libs/tests/GuestJpegDec.cpp +++ b/core/libs/tests/GuestJpegDec.cpp @@ -1,12 +1,14 @@ #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include "Decoder/Jpeg.hpp" +#include #include #include #include #include #include #include +#include #include static_assert(sizeof(JpegDecCreateParam) == 0xC); @@ -80,8 +82,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 +138,23 @@ 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); })); + std::vector invalidAlphaImage(7 * 5 * 4, 0xCC); + JpegDecDecodeParam invalidAlpha = DecodeParam(jpeg, invalidAlphaImage, 7 * 4); + invalidAlpha.alpha_value = 0x100; + Require(sceJpegDecDecode(handle, &invalidAlpha, &info) == invalidParam); + Require(std::all_of(invalidAlphaImage.begin(), invalidAlphaImage.end(), [](std::uint8_t byte) { return byte == 0xCC; })); + invalidAlpha.alpha_value = 0xFFFF; + Require(sceJpegDecDecode(handle, &invalidAlpha, &info) == invalidParam); + Require(std::all_of(invalidAlphaImage.begin(), invalidAlphaImage.end(), [](std::uint8_t byte) { return byte == 0xCC; })); + std::vector misalignedImage(7 * 5 * 4 + 1, 0xCC); + JpegDecDecodeParam misaligned = DecodeParam(jpeg, misalignedImage, 7 * 4); + misaligned.image_mem_addr = misalignedImage.data() + 1; + misaligned.image_mem_size = static_cast(misalignedImage.size() - 1); + Require(sceJpegDecDecode(handle, &misaligned, &info) == invalidAddr); + Require(std::all_of(misalignedImage.begin(), misalignedImage.end(), [](std::uint8_t byte) { return byte == 0xCC; })); JpegDecDecodeParam otherMode = decodeParam; otherMode.decode_mode = 1; Require(ThrowsRuntimeError([&] { sceJpegDecDecode(handle, &otherMode, &info); })); @@ -157,12 +184,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/GuestJson.cpp b/core/libs/tests/GuestJson.cpp index 7c438e3867..16de775009 100644 --- a/core/libs/tests/GuestJson.cpp +++ b/core/libs/tests/GuestJson.cpp @@ -45,6 +45,7 @@ void APS5_VABI _ZN3sce4Json6StringC1EPKc(String*, const char*); void APS5_VABI _ZN3sce4Json6StringD1Ev(String*); const char* APS5_VABI _ZNK3sce4Json6String5c_strEv(const String*); std::size_t APS5_VABI _ZNK3sce4Json6String6lengthEv(const String*); +bool APS5_VABI _ZNK3sce4Json6String5emptyEv(const String*); void APS5_VABI _ZN3sce4Json5ArrayC1Ev(Array*); void APS5_VABI _ZN3sce4Json5ArrayC1ERKS1_(Array*, const Array*); void APS5_VABI _ZN3sce4Json5Array5clearEv(Array*); @@ -71,15 +72,25 @@ Pair* APS5_VABI _ZNK3sce4Json6Object8iteratordeEv(const Iterator*); Pair* APS5_VABI _ZNK3sce4Json6Object8iteratorptEv(const Iterator*); Iterator* APS5_VABI _ZNK3sce4Json6Object4findERKNS0_6StringE(Iterator*, const Object*, const String*); int APS5_VABI _ZNK3sce4Json6String7compareEPKc(const String*, const char*); +int APS5_VABI _ZNK3sce4Json6String7compareERKS1_(const String*, const String*); +std::size_t APS5_VABI _ZNK3sce4Json6String4sizeEv(const String*); +std::size_t APS5_VABI _ZNK3sce4Json6String8capacityEv(const String*); +bool APS5_VABI _ZNK3sce4Json6StringeqEPKc(const String*, const char*); +bool APS5_VABI _ZNK3sce4Json6StringeqERKS1_(const String*, const String*); extern std::size_t _ZN3sce4Json6String4nposE; void APS5_VABI _ZN3sce4Json6String5clearEv(String*); +void APS5_VABI _ZN3sce4Json6String6resizeEm(String*, std::size_t); void APS5_VABI _ZN3sce4Json6String7reserveEm(String*, std::size_t); String* APS5_VABI _ZN3sce4Json6String6appendERKS1_(String*, const String*); String* APS5_VABI _ZN3sce4Json6String6appendEPKc(String*, const char*); +String* APS5_VABI _ZN3sce4Json6String6appendEPKcm(String*, const char*, std::size_t); +String* APS5_VABI _ZN3sce4Json6StringpLEPKc(String*, const char*); +String* APS5_VABI _ZN3sce4Json6StringpLEh(String*, unsigned char); String* APS5_VABI _ZNK3sce4Json6String6substrEmm(String*, const String*, std::size_t, std::size_t); std::size_t APS5_VABI _ZNK3sce4Json6String4findEcm(const String*, char, std::size_t); std::size_t APS5_VABI _ZNK3sce4Json6String5rfindEcm(const String*, char, std::size_t); bool APS5_VABI _ZNK3sce4Json6Object8iteratorneERKS2_(const Iterator*, const Iterator*); +bool APS5_VABI _ZNK3sce4Json6Object8iteratoreqERKS2_(const Iterator*, const Iterator*); void APS5_VABI _ZN3sce4Json5ValueC1Ev(Value*); void APS5_VABI _ZN3sce4Json5ValueC1Eb(Value*, bool); void APS5_VABI _ZN3sce4Json5ValueC1Ed(Value*, double); @@ -117,12 +128,24 @@ int APS5_VABI _ZN3sce4Json11Initializer10initializeEPKNS0_14InitParameter2E(void void APS5_VABI _ZN3sce4Json18InitParameterRtti216setAllocatorRttiEPNS0_14AllocParamRttiEPv(void*, void*, void*); void APS5_VABI _ZN3sce4Json14InitParameter225setSpecialFloatFormatTypeENS0_22SpecialFloatFormatTypeE(void*, std::int32_t); void APS5_VABI _ZN3sce4Json17InitParameterRttiC1EPNS0_14AllocParamRttiEPvm(void*, void*, void*, std::size_t); +void APS5_VABI _ZN3sce4Json6Object4PairC1Ev(Pair*); void APS5_VABI _ZN3sce4Json6Object4PairC1ERKNS0_6StringERKNS0_5ValueE(Pair*, const String*, const Value*); void APS5_VABI _ZN3sce4Json6Object4PairD1Ev(Pair*); +Iterator* APS5_VABI _ZN3sce4Json6Object6insertERKNS1_4PairE(Iterator*, Object*, const Pair*); +void APS5_VABI _ZN3sce4Json6Object8iteratorC1Ev(Iterator*); +Iterator* APS5_VABI _ZN3sce4Json6Object8iteratoraSERKS2_(Iterator*, const Iterator*); +Iterator* APS5_VABI _ZN3sce4Json6Object8iteratorppEi(Iterator*, Iterator*, int); void APS5_VABI _ZN3sce4Json5Array8iteratorC1Ev(Iterator*); Iterator* APS5_VABI _ZN3sce4Json5Array8iteratoraSERKS2_(Iterator*, const Iterator*); Iterator* APS5_VABI _ZN3sce4Json5Array8iteratorppEi(Iterator*, Iterator*, int); void APS5_VABI _ZN3sce4Json12MemAllocator11notifyErrorEimPv(void*, int, std::size_t, void*); +bool APS5_VABI _ZNK3sce4Json5ValuecvbEv(const Value*); +void APS5_VABI _ZN3sce4Json5Value4swapERS1_(Value*, Value*); +void APS5_VABI _ZN3sce4Json5Array8iteratorC1ERKS2_(Iterator*, const Iterator*); +Iterator* APS5_VABI _ZN3sce4Json5Array8iterator7advanceEm(Iterator*, std::size_t); +void APS5_VABI _ZN3sce4Json6Object8iteratorC1ERKS2_(Iterator*, const Iterator*); +Iterator* APS5_VABI _ZN3sce4Json6Object8iterator7advanceEm(Iterator*, std::size_t); +std::size_t APS5_VABI _ZN3sce4Json6Object5eraseERKNS0_6StringE(Object*, const String*); } static void Check(bool value, int line) { @@ -453,6 +476,105 @@ static void ObjectFind() { _ZN3sce4Json6ObjectD1Ev(&object); } +static void ObjectIteratorCopies() { + Object object{}; + _ZN3sce4Json6ObjectC1Ev(&object); + String key1{}, key2{}; + _ZN3sce4Json6StringC1EPKc(&key1, "one"); + _ZN3sce4Json6StringC1EPKc(&key2, "two"); + Value val1{}, val2{}; + _ZN3sce4Json5ValueC1Ev(&val1); + _ZN3sce4Json5ValueC1Ev(&val2); + Require(_ZN3sce4Json5Value3setEl(&val1, 10) == 0); + Require(_ZN3sce4Json5Value3setEl(&val2, 20) == 0); + Pair p1{}, p2{}; + _ZN3sce4Json6Object4PairC1ERKNS0_6StringERKNS0_5ValueE(&p1, &key1, &val1); + _ZN3sce4Json6Object4PairC1ERKNS0_6StringERKNS0_5ValueE(&p2, &key2, &val2); + Iterator ins1{}, ins2{}; + Require(_ZN3sce4Json6Object6insertERKNS1_4PairE(&ins1, &object, &p1) == &ins1); + Require(_ZN3sce4Json6Object6insertERKNS1_4PairE(&ins2, &object, &p2) == &ins2); + _ZN3sce4Json6Object8iteratorD1Ev(&ins1); + _ZN3sce4Json6Object8iteratorD1Ev(&ins2); + + Iterator it{}; + std::memset(&it, 0x5a, sizeof(it)); + _ZN3sce4Json6Object8iteratorC1Ev(&it); + Require(it.position == nullptr); + + Iterator begin{}, end{}; + _ZNK3sce4Json6Object5beginEv(&begin, &object); + _ZNK3sce4Json6Object3endEv(&end, &object); + + Require(!_ZNK3sce4Json6Object8iteratoreqERKS2_(&begin, &end)); + Require(_ZNK3sce4Json6Object8iteratorneERKS2_(&begin, &end)); + + Iterator post{}; + Require(_ZN3sce4Json6Object8iteratorppEi(&post, &begin, 0) == &post); + Require(std::strcmp(_ZNK3sce4Json6String5c_strEv(&_ZNK3sce4Json6Object8iteratordeEv(&post)->key), "one") == 0); + Require(std::strcmp(_ZNK3sce4Json6String5c_strEv(&_ZNK3sce4Json6Object8iteratordeEv(&begin)->key), "two") == 0); + _ZN3sce4Json6Object8iteratorD1Ev(&post); + + Iterator assigned{}; + Require(_ZN3sce4Json6Object8iteratoraSERKS2_(&assigned, &begin) == &assigned); + Require(_ZNK3sce4Json6Object8iteratoreqERKS2_(&assigned, &begin)); + Require(!_ZNK3sce4Json6Object8iteratorneERKS2_(&assigned, &begin)); + Require(std::strcmp(_ZNK3sce4Json6String5c_strEv(&_ZNK3sce4Json6Object8iteratordeEv(&assigned)->key), "two") == 0); + _ZN3sce4Json6Object8iteratorD1Ev(&assigned); + + _ZN3sce4Json6Object8iteratorD1Ev(&begin); + _ZN3sce4Json6Object8iteratorD1Ev(&end); + _ZN3sce4Json6Object4PairD1Ev(&p1); + _ZN3sce4Json6Object4PairD1Ev(&p2); + _ZN3sce4Json5ValueD1Ev(&val1); + _ZN3sce4Json5ValueD1Ev(&val2); + _ZN3sce4Json6StringD1Ev(&key1); + _ZN3sce4Json6StringD1Ev(&key2); + _ZN3sce4Json6ObjectD1Ev(&object); +} + +static void ObjectInsert() { + Object object{}; + _ZN3sce4Json6ObjectC1Ev(&object); + String key{}; + _ZN3sce4Json6StringC1EPKc(&key, "score"); + Value value{}; + _ZN3sce4Json5ValueC1Ev(&value); + Require(_ZN3sce4Json5Value3setEl(&value, 100) == 0); + Pair pair{}; + _ZN3sce4Json6Object4PairC1ERKNS0_6StringERKNS0_5ValueE(&pair, &key, &value); + + Iterator inserted{}, end{}; + _ZNK3sce4Json6Object3endEv(&end, &object); + Require(_ZN3sce4Json6Object6insertERKNS1_4PairE(&inserted, &object, &pair) == &inserted); + Require(_ZNK3sce4Json6Object8iteratorneERKS2_(&inserted, &end)); + Require(_ZNK3sce4Json6Object4sizeEv(&object) == 1); + const Pair* p = _ZNK3sce4Json6Object8iteratordeEv(&inserted); + Require(std::strcmp(_ZNK3sce4Json6String5c_strEv(&p->key), "score") == 0); + Require(*_ZNK3sce4Json5Value10getIntegerEv(&p->value) == 100); + + String otherKey{}; + _ZN3sce4Json6StringC1EPKc(&otherKey, "other"); + Value otherValue{}; + _ZN3sce4Json5ValueC1Ev(&otherValue); + Require(_ZN3sce4Json5Value3setEPKc(&otherValue, "str") == 0); + Pair otherPair{}; + _ZN3sce4Json6Object4PairC1ERKNS0_6StringERKNS0_5ValueE(&otherPair, &otherKey, &otherValue); + Iterator otherInserted{}; + Require(_ZN3sce4Json6Object6insertERKNS1_4PairE(&otherInserted, &object, &otherPair) == &otherInserted); + Require(_ZNK3sce4Json6Object4sizeEv(&object) == 2); + + _ZN3sce4Json6Object8iteratorD1Ev(&otherInserted); + _ZN3sce4Json6Object4PairD1Ev(&otherPair); + _ZN3sce4Json5ValueD1Ev(&otherValue); + _ZN3sce4Json6StringD1Ev(&otherKey); + _ZN3sce4Json6Object8iteratorD1Ev(&inserted); + _ZN3sce4Json6Object8iteratorD1Ev(&end); + _ZN3sce4Json6Object4PairD1Ev(&pair); + _ZN3sce4Json5ValueD1Ev(&value); + _ZN3sce4Json6StringD1Ev(&key); + _ZN3sce4Json6ObjectD1Ev(&object); +} + static void StringMethods() { Require(_ZN3sce4Json6String4nposE == static_cast(-1)); const auto text = [](const String& string) { return std::string(_ZNK3sce4Json6String5c_strEv(&string), _ZNK3sce4Json6String6lengthEv(&string)); }; @@ -479,18 +601,125 @@ static void StringMethods() { refused = true; } Require(refused); + Require(_ZNK3sce4Json6String4sizeEv(&base) == 5); + Require(_ZNK3sce4Json6String6lengthEv(&base) == 5); + String match{}; + _ZN3sce4Json6StringC1EPKc(&match, "a/b/c"); + Require(_ZNK3sce4Json6StringeqERKS1_(&base, &match)); + Require(_ZNK3sce4Json6String7compareERKS1_(&base, &match) == 0); + _ZN3sce4Json6StringD1Ev(&match); _ZN3sce4Json6String7reserveEm(&base, 64); + Require(_ZNK3sce4Json6String8capacityEv(&base) >= 64); Require(_ZN3sce4Json6String6appendERKS1_(&base, &suffix) == &base); Require(_ZN3sce4Json6String6appendEPKc(&base, "!") == &base); - Require(text(base) == "a/b/c-tail!"); + Require(_ZN3sce4Json6String6appendEPKcm(&base, "12345", 2) == &base); + Require(_ZN3sce4Json6StringpLEPKc(&base, "+") == &base); + Require(_ZN3sce4Json6StringpLEh(&base, '?') == &base); + Require(text(base) == "a/b/c-tail!12+?"); + _ZN3sce4Json6String6resizeEm(&base, 5); + Require(_ZNK3sce4Json6String4sizeEv(&base) == 5); + Require(text(base) == "a/b/c"); _ZN3sce4Json6String6appendERKS1_(&base, &base); - Require(text(base) == "a/b/c-tail!a/b/c-tail!"); + Require(text(base) == "a/b/ca/b/c"); _ZN3sce4Json6String5clearEv(&base); Require(text(base).empty()); _ZN3sce4Json6StringD1Ev(&suffix); _ZN3sce4Json6StringD1Ev(&base); } +static void ValueSwap() { + Value v1{}, v2{}; + _ZN3sce4Json5ValueC1Ev(&v1); + _ZN3sce4Json5ValueC1Ev(&v2); + Require(_ZN3sce4Json5Value3setEl(&v1, 123) == 0); + Require(_ZN3sce4Json5Value3setEPKc(&v2, "hello") == 0); + _ZN3sce4Json5Value4swapERS1_(&v1, &v2); + Require(*_ZNK3sce4Json5Value10getIntegerEv(&v2) == 123); + Require(std::strcmp(_ZNK3sce4Json6String5c_strEv(_ZNK3sce4Json5Value9getStringEv(&v1)), "hello") == 0); + _ZN3sce4Json5ValueD1Ev(&v1); + _ZN3sce4Json5ValueD1Ev(&v2); +} + +static void ArrayIteratorAdvance() { + Array array{}; + _ZN3sce4Json5ArrayC1Ev(&array); + for (int i = 0; i < 5; ++i) { + Value val{}; + _ZN3sce4Json5ValueC1Ev(&val); + _ZN3sce4Json5Value3setEl(&val, i * 10); + _ZN3sce4Json5Array9push_backERKNS0_5ValueE(&array, &val); + _ZN3sce4Json5ValueD1Ev(&val); + } + Iterator begin{}; + _ZNK3sce4Json5Array5beginEv(&begin, &array); + Iterator copy{}; + _ZN3sce4Json5Array8iteratorC1ERKS2_(©, &begin); + Require(_ZN3sce4Json5Array8iterator7advanceEm(©, 3) == ©); + Require(*_ZNK3sce4Json5Value10getIntegerEv(_ZNK3sce4Json5Array8iteratordeEv(©)) == 30); + _ZN3sce4Json5Array8iteratorD1Ev(&begin); + _ZN3sce4Json5Array8iteratorD1Ev(©); + _ZN3sce4Json5ArrayD1Ev(&array); +} + +static void ObjectOperations() { + Object object{}; + _ZN3sce4Json6ObjectC1Ev(&object); + String k1{}, k2{}, k3{}; + _ZN3sce4Json6StringC1EPKc(&k1, "alpha"); + _ZN3sce4Json6StringC1EPKc(&k2, "beta"); + _ZN3sce4Json6StringC1EPKc(&k3, "gamma"); + Value v1{}, v2{}, v3{}; + _ZN3sce4Json5ValueC1Ev(&v1); + _ZN3sce4Json5ValueC1Ev(&v2); + _ZN3sce4Json5ValueC1Ev(&v3); + _ZN3sce4Json5Value3setEl(&v1, 1); + _ZN3sce4Json5Value3setEl(&v2, 2); + _ZN3sce4Json5Value3setEl(&v3, 3); + Pair p1{}, p2{}, p3{}; + _ZN3sce4Json6Object4PairC1ERKNS0_6StringERKNS0_5ValueE(&p1, &k1, &v1); + _ZN3sce4Json6Object4PairC1ERKNS0_6StringERKNS0_5ValueE(&p2, &k2, &v2); + _ZN3sce4Json6Object4PairC1ERKNS0_6StringERKNS0_5ValueE(&p3, &k3, &v3); + Iterator ins{}; + _ZN3sce4Json6Object6insertERKNS1_4PairE(&ins, &object, &p1); + _ZN3sce4Json6Object8iteratorD1Ev(&ins); + _ZN3sce4Json6Object6insertERKNS1_4PairE(&ins, &object, &p2); + _ZN3sce4Json6Object8iteratorD1Ev(&ins); + _ZN3sce4Json6Object6insertERKNS1_4PairE(&ins, &object, &p3); + _ZN3sce4Json6Object8iteratorD1Ev(&ins); + + Iterator begin{}; + _ZNK3sce4Json6Object5beginEv(&begin, &object); + Iterator copy{}; + _ZN3sce4Json6Object8iteratorC1ERKS2_(©, &begin); + Require(_ZN3sce4Json6Object8iterator7advanceEm(©, 2) == ©); + Require(std::strcmp(_ZNK3sce4Json6String5c_strEv(&_ZNK3sce4Json6Object8iteratordeEv(©)->key), "gamma") == 0); + _ZN3sce4Json6Object8iteratorD1Ev(&begin); + _ZN3sce4Json6Object8iteratorD1Ev(©); + + Require(_ZNK3sce4Json6Object4sizeEv(&object) == 3); + Require(_ZN3sce4Json6Object5eraseERKNS0_6StringE(&object, &k2) == 1); + Require(_ZNK3sce4Json6Object4sizeEv(&object) == 2); + Require(_ZN3sce4Json6Object5eraseERKNS0_6StringE(&object, &k2) == 0); + + Iterator findResult{}, end{}; + _ZNK3sce4Json6Object4findERKNS0_6StringE(&findResult, &object, &k2); + _ZNK3sce4Json6Object3endEv(&end, &object); + Require(_ZNK3sce4Json6Object8iteratoreqERKS2_(&findResult, &end)); + _ZN3sce4Json6Object8iteratorD1Ev(&findResult); + _ZN3sce4Json6Object8iteratorD1Ev(&end); + + _ZN3sce4Json6Object4PairD1Ev(&p1); + _ZN3sce4Json6Object4PairD1Ev(&p2); + _ZN3sce4Json6Object4PairD1Ev(&p3); + _ZN3sce4Json5ValueD1Ev(&v1); + _ZN3sce4Json5ValueD1Ev(&v2); + _ZN3sce4Json5ValueD1Ev(&v3); + _ZN3sce4Json6StringD1Ev(&k1); + _ZN3sce4Json6StringD1Ev(&k2); + _ZN3sce4Json6StringD1Ev(&k3); + _ZN3sce4Json6ObjectD1Ev(&object); +} + static void NullAccess(SetNullAccessCallback setCallback) { callbackCalls = 0; lastRequested = -1; @@ -931,15 +1160,49 @@ static void ValueSpecialFloatHandler() { _ZN3sce4Json5ValueD1Ev(&real); } +static void ValueOperatorBool() { + Value nullVal{}; + _ZN3sce4Json5ValueC1Ev(&nullVal); + Require(!_ZNK3sce4Json5ValuecvbEv(&nullVal)); + _ZN3sce4Json5ValueD1Ev(&nullVal); + + Value typedNull{}; + _ZN3sce4Json5ValueC1ENS0_9ValueTypeE(&typedNull, TypeNull); + Require(!_ZNK3sce4Json5ValuecvbEv(&typedNull)); + _ZN3sce4Json5ValueD1Ev(&typedNull); + + Value boolFalse{}; + _ZN3sce4Json5ValueC1Eb(&boolFalse, false); + Require(!_ZNK3sce4Json5ValuecvbEv(&boolFalse)); + _ZN3sce4Json5ValueD1Ev(&boolFalse); + + Value boolTrue{}; + _ZN3sce4Json5ValueC1Eb(&boolTrue, true); + Require(_ZNK3sce4Json5ValuecvbEv(&boolTrue)); + _ZN3sce4Json5ValueD1Ev(&boolTrue); + + Value cleared{}; + _ZN3sce4Json5ValueC1Eb(&cleared, true); + Require(_ZNK3sce4Json5ValuecvbEv(&cleared)); + _ZN3sce4Json5Value5clearEv(&cleared); + Require(!_ZNK3sce4Json5ValuecvbEv(&cleared)); + _ZN3sce4Json5ValueD1Ev(&cleared); +} + int main() { ParseAndRoundTrip(); NestingDepth(); ObjectsAndArrays(); ObjectFind(); ObjectPair(); + ObjectInsert(); ArrayIteratorCopies(); + ObjectIteratorCopies(); AllocatorNotifyError(); StringMethods(); + ValueSwap(); + ArrayIteratorAdvance(); + ObjectOperations(); NullAccess(_ZN3sce4Json11Initializer27setGlobalNullAccessCallBackEPFRKNS0_5ValueENS0_9ValueTypeEPS3_PvES7_); NullAccess(_ZN3sce4Json11Initializer27setGlobalNullAccessCallbackEPFRKNS0_5ValueENS0_9ValueTypeEPS3_PvES7_); StaticNullAccess(); @@ -950,5 +1213,6 @@ int main() { InitParameter2BaseConstructor(); InitParameters(); ValueClear(); + ValueOperatorBool(); StreamParse(); } diff --git a/core/libs/tests/GuestKernelErrors.cpp b/core/libs/tests/GuestKernelErrors.cpp index b1981c7ee0..1932625356 100644 --- a/core/libs/tests/GuestKernelErrors.cpp +++ b/core/libs/tests/GuestKernelErrors.cpp @@ -34,6 +34,7 @@ int APS5_VABI scePthreadMutexUnlock(PthreadMutex* mutex); int APS5_VABI scePthreadMutexTrylock(PthreadMutex* mutex); int APS5_VABI sceKernelCreateEventFlag(KernelEventFlag* ef, const char* name, std::uint32_t attr, std::uint64_t init_pattern, const void* param); int APS5_VABI sceKernelDeleteEventFlag(KernelEventFlag ef); +int APS5_VABI sceKernelOpenEventFlag(KernelEventFlag* ef, const char* name); int APS5_VABI sceKernelSetEventFlag(KernelEventFlag ef, std::uint64_t bit_pattern); int APS5_VABI sceKernelClearEventFlag(KernelEventFlag ef, std::uint64_t bit_pattern); int APS5_VABI sceKernelCancelEventFlag(KernelEventFlag ef, std::uint64_t set_pattern, int* num_wait_threads); @@ -204,6 +205,19 @@ int main() { Require(sceKernelWaitEventFlag(nullptr, 1, EVF_WAITMODE_OR, nullptr, nullptr) == SCE_KERNEL_ERROR_ESRCH); Require(sceKernelCancelEventFlag(nullptr, 0, nullptr) == SCE_KERNEL_ERROR_ESRCH); + KernelEventFlag opened = nullptr; + Require(sceKernelOpenEventFlag(nullptr, "test") == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelOpenEventFlag(&opened, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelOpenEventFlag(&opened, "SceShellCoreUtilAppFocus") == SCE_KERNEL_ERROR_ENOENT && opened == nullptr); + Require(sceKernelCreateEventFlag(&ef, "test_open", EVF_ATTR_MULTI, 0x05, nullptr) == SCE_OK); + Require(sceKernelOpenEventFlag(&opened, "test_open") == SCE_OK && opened == ef); + Require(sceKernelSetEventFlag(opened, 0x08) == SCE_OK); + std::uint64_t openedPattern = 0; + Require(sceKernelPollEventFlag(ef, 0x08, EVF_WAITMODE_OR, &openedPattern) == SCE_OK && openedPattern == 0x0d); + Require(sceKernelDeleteEventFlag(ef) == SCE_OK); + opened = nullptr; + Require(sceKernelOpenEventFlag(&opened, "test_open") == SCE_KERNEL_ERROR_ENOENT && opened == nullptr); + Require(sceKernelCreateEventFlag(&ef, "flag", EVF_ATTR_MULTI, 0x15, nullptr) == SCE_OK); std::uint64_t pattern = 0; Require(sceKernelPollEventFlag(ef, 0, EVF_WAITMODE_OR, &pattern) == SCE_KERNEL_ERROR_EINVAL); diff --git a/core/libs/tests/GuestKernelFcntl.cpp b/core/libs/tests/GuestKernelFcntl.cpp index 6d555d64eb..b75d3c22a9 100644 --- a/core/libs/tests/GuestKernelFcntl.cpp +++ b/core/libs/tests/GuestKernelFcntl.cpp @@ -9,6 +9,11 @@ #include #include #include +#ifndef _WIN32 +#include +#include +#include +#endif extern "C" { int APS5_VABI socket_nid_postfix(int, int, int); int APS5_VABI __sys_socketex_nid_postfix(const char*, int, int, int); @@ -23,6 +28,7 @@ int* APS5_VABI __error_nid_postfix(); int APS5_VABI sceKernelFcntl(int, int, ...); int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); int APS5_VABI sceKernelClose(int); +std::int64_t APS5_VABI sceKernelRead(int, void*, std::size_t); std::int64_t APS5_VABI sceKernelLseek(int, std::int64_t, int); } static void Require(bool value) { if (!value) std::abort(); } @@ -34,12 +40,14 @@ static constexpr int SetFlags = 4; static constexpr int OldGetLock = 7; static constexpr int OldSetLock = 8; static constexpr int OldSetLockWait = 9; +static constexpr int Duplicate2Command = 10; static constexpr int GetLock = 11; static constexpr int SetLock = 12; static constexpr int SetLockWait = 13; static constexpr int ReadAhead = 15; static constexpr int DarwinReadAhead = 16; static constexpr int DuplicateCloseOnExec = 17; +static constexpr int Duplicate2CloseOnExec = 18; static constexpr int ReadOnly = 0; static constexpr int WriteOnly = 1; static constexpr int ReadWrite = 2; @@ -67,6 +75,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); @@ -94,6 +105,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); @@ -102,13 +121,76 @@ static void SocketsKeepTheirPath() { Require(sceKernelFcntl(socket, GetFlags) == (ReadWrite | NonBlocking)); Require(sceKernelFcntl(socket, SetFlags, 0) == 0); Require(sceKernelFcntl(socket, GetFlags) == ReadWrite); + GuestLock lock{}; + Require(sceKernelFcntl(socket, GetLock, &lock) == SceEinval); + Require(sceKernelFcntl(socket, GetDescriptorFlags) == 0); + Require(sceKernelFcntl(socket, SetDescriptorFlags, 1) == 0); + Require(sceKernelFcntl(socket, GetDescriptorFlags) == 1); + const int duplicate = sceKernelFcntl(socket, DuplicateCommand, 20); + Require(duplicate >= 20); + Require(sceKernelFcntl(duplicate, GetDescriptorFlags) == 0); + Require(sceKernelFcntl(socket, GetDescriptorFlags) == 1); + Require(sceKernelFcntl(socket, SetFlags, ReadWrite | NonBlocking) == 0); + Require(sceKernelFcntl(duplicate, GetFlags) == (ReadWrite | NonBlocking)); + Require(sceKernelFcntl(duplicate, SetFlags, 0) == 0); + Require(sceKernelFcntl(socket, GetFlags) == ReadWrite); + const int closing = sceKernelFcntl(socket, DuplicateCloseOnExec, duplicate + 1); + Require(closing >= duplicate + 1); + Require(sceKernelFcntl(closing, GetDescriptorFlags) == 1); + Require(sceKernelFcntl(socket, SetDescriptorFlags, 0) == 0); + Require(sceKernelFcntl(socket, GetDescriptorFlags) == 0); + Require(fcntl_nid_postfix(socket, Duplicate2Command, closing) == closing); + Require(sceKernelFcntl(closing, GetDescriptorFlags) == 0); + Require(sceKernelFcntl(socket, Duplicate2CloseOnExec, closing) == closing); + Require(sceKernelFcntl(closing, GetDescriptorFlags) == 1); + Require(sceKernelFcntl(socket, GetDescriptorFlags) == 0); + Require(sceKernelFcntl(socket, Duplicate2CloseOnExec, socket) == socket); + Require(sceKernelFcntl(socket, GetDescriptorFlags) == 1); + Require(sceKernelFcntl(socket, Duplicate2Command, socket) == socket); + Require(sceKernelFcntl(socket, GetDescriptorFlags) == 1); + Require(sceKernelFcntl(socket, SetDescriptorFlags, 0) == 0); + int pipe[2] = {-1, -1}; + Require(pipe_nid_postfix(pipe) == 0); + Require(fcntl_nid_postfix(pipe[0], SetDescriptorFlags, 1) == 0); + Require(fcntl_nid_postfix(socket, Duplicate2Command, pipe[0]) == pipe[0]); + Require(sceKernelFcntl(pipe[0], GetFlags) == ReadWrite); + Require(sceKernelFcntl(pipe[0], GetDescriptorFlags) == 0); + Require(sceKernelFcntl(socket, Duplicate2CloseOnExec, pipe[1]) == pipe[1]); + Require(sceKernelFcntl(pipe[1], GetFlags) == ReadWrite); + Require(sceKernelFcntl(pipe[1], GetDescriptorFlags) == 1); + Require(close_nid_postfix(pipe[0]) == 0 && close_nid_postfix(pipe[1]) == 0); + const int random = sceKernelOpen("/dev/urandom", ReadOnly, 0); + Require(random >= 0); + Require(fcntl_nid_postfix(socket, Duplicate2Command, random) == random); + Require(sceKernelFcntl(random, GetFlags) == ReadWrite); + Require(close_nid_postfix(random) == 0); + const int file = sceKernelOpen("kernel_fcntl_socket_dup2.bin", CreateReadWrite, 0644); + Require(file == random); + char byte = 0; + Require(sceKernelRead(file, &byte, 1) == 0); + Require(sceKernelFcntl(socket, Duplicate2CloseOnExec, file) == file); + Require(sceKernelFcntl(file, GetFlags) == ReadWrite); + Require(sceKernelFcntl(file, GetDescriptorFlags) == 1); + Require(close_nid_postfix(file) == 0); + std::remove("kernel_fcntl_socket_dup2.bin"); + RequireFailure(fcntl_nid_postfix(socket, DuplicateCommand, -1), GuestEinval); + RequireFailure(fcntl_nid_postfix(socket, DuplicateCloseOnExec, -1), GuestEinval); + RequireFailure(fcntl_nid_postfix(socket, Duplicate2Command, -1), GuestEbadf); + RequireFailure(fcntl_nid_postfix(socket, Duplicate2CloseOnExec, -1), GuestEbadf); + Require(sceKernelFcntl(socket, DuplicateCommand, -1) == SceEinval); + Require(sceKernelFcntl(socket, DuplicateCloseOnExec, -1) == SceEinval); + Require(sceKernelFcntl(socket, Duplicate2Command, -1) == SceEbadf); + Require(sceKernelFcntl(socket, Duplicate2CloseOnExec, -1) == SceEbadf); Require(sceKernelFcntl(socket, SetFlags, 0x8000) == SceEnotsup); Require(sceKernelFcntl(socket, GetFlags) == ReadWrite); Require(sceKernelFcntl(socket, 5) == SceEinval); Require(sceKernelFcntl(socket + 4096, GetFlags) == SceEbadf); int queued = -1; Require(ioctl_nid_postfix(socket, ReadableRequest, &queued) == 0 && queued == 0); - Require(close_nid_postfix(socket) == 0); + RequireMalformedRequestsFail(socket); + RequireFailure(ioctl_nid_postfix(socket, IntegerArgumentRequest, nullptr), GuestEfault); + RequireFailure(ioctl_nid_postfix(socket, EmptyInputRequest, nullptr), GuestEfault); + for (const int descriptor : {closing, duplicate, socket}) Require(close_nid_postfix(descriptor) == 0); Require(sceKernelFcntl(socket, GetFlags) == SceEbadf); Require(sceKernelFcntl(-1, GetFlags) == SceEbadf); Require(sceKernelFcntl(12345, GetFlags) == SceEbadf); @@ -159,6 +241,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")); @@ -183,6 +266,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); @@ -381,10 +466,32 @@ 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); } +#ifndef _WIN32 +static void HostSocketFlags() { + int sockets[2] = {-1, -1}; + Require(::socketpair(AF_UNIX, SOCK_STREAM, 0, sockets) == 0); + Require(fcntl_nid_postfix(sockets[0], GetFlags) == ReadWrite); + Require(fcntl_nid_postfix(sockets[0], SetFlags, NonBlocking) == 0); + Require(fcntl_nid_postfix(sockets[0], GetFlags) == (ReadWrite | NonBlocking)); + Require((::fcntl(sockets[0], F_GETFL) & O_NONBLOCK) != 0); + Require(fcntl_nid_postfix(sockets[0], SetDescriptorFlags, 1) == 0); + Require(fcntl_nid_postfix(sockets[0], GetDescriptorFlags) == 1); + Require(::write(sockets[1], "abc", 3) == 3); + int available = 0; + Require(ioctl_nid_postfix(sockets[0], ReadableRequest, &available) == 0 && available == 3); + GuestLock lock{0, 0, 0, WriteLock, FromStart, 0}; + RequireFailure(fcntl_nid_postfix(sockets[0], SetLock, &lock), GuestEbadf); + RequireFailure(fcntl_nid_postfix(sockets[0], DarwinReadAhead, 1), GuestEbadf); + Require(ThrowsWith([&sockets] { fcntl_nid_postfix(sockets[0], SetFlags, NonBlocking | 0x40); }, "fcntl: O_ASYNC on a socket is not implemented")); + for (const int socket : sockets) Require(close_nid_postfix(socket) == 0); +} +#endif + static void StandardInputFlags() { const int flags = fcntl_nid_postfix(0, GetFlags); Require(flags >= 0); @@ -402,6 +509,9 @@ int main() { FileFlagsRoundTrip(); RecordLocksBelongToTheProcess(); PipeFlagsRoundTrip(); +#ifndef _WIN32 + HostSocketFlags(); +#endif StandardInputFlags(); return 0; } diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index a00be4c9e3..a60abca020 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -1,4 +1,6 @@ +#include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/RemoveTree.hpp" #include "prx/libkernel/File/include/FileFlags.hpp" #include "SceTypes.hpp" #include @@ -54,6 +56,7 @@ int APS5_VABI mkdirat_nid_postfix(int, const char*, std::uint16_t); int APS5_VABI renameat_nid_postfix(int, const char*, int, const char*); int APS5_VABI fchmodat_nid_postfix(int, const char*, int, int); int APS5_VABI fchown_nid_postfix(int, std::uint32_t, std::uint32_t); +int APS5_VABI fstatat_nid_postfix(int, const char*, FileStat*, int); std::int64_t APS5_VABI sceKernelRead(int, void*, std::size_t); std::uint16_t APS5_VABI umask_nid_postfix(std::uint16_t); int* APS5_VABI __error_nid_postfix(); @@ -295,8 +298,16 @@ int main() { Require(readlink_nid_postfix(file.c_str(), buffer, sizeof(buffer)) == -1 && Error() == 22); Require(readlink_nid_postfix(root.string().c_str(), buffer, sizeof(buffer)) == -1 && Error() == 22); Require(readlink_nid_postfix((root / "missing").string().c_str(), buffer, sizeof(buffer)) == -1 && Error() == 2); + Require(readlink_nid_postfix((root / "file.txt" / "child").string().c_str(), buffer, sizeof(buffer)) == -1 && Error() == 20); Require(readlink_nid_postfix("", buffer, sizeof(buffer)) == -1 && Error() == 2); Require(readlink_nid_postfix(nullptr, buffer, sizeof(buffer)) == -1 && Error() == 14); + constexpr auto hugeSize = static_cast(-1); + Require(readlink_nid_postfix(nullptr, buffer, hugeSize) == -1 && Error() == 22); + Require(readlink_nid_postfix("", buffer, hugeSize) == -1 && Error() == 22); + Require(readlink_nid_postfix((root / "missing").string().c_str(), buffer, hugeSize) == -1 && Error() == 22); + Require(readlinkat_nid_postfix(-100, nullptr, buffer, hugeSize) == -1 && Error() == 22); + Require(readlinkat_nid_postfix(-100, "", buffer, hugeSize) == -1 && Error() == 22); + Require(readlinkat_nid_postfix(-100, nullptr, buffer, sizeof(buffer)) == -1 && Error() == 14); #ifndef _WIN32 Require(readlink_nid_postfix((root / std::string(300, 'n')).string().c_str(), buffer, sizeof(buffer)) == -1 && Error() == 63); #endif @@ -478,6 +489,12 @@ int main() { Require(sceKernelClose(made) == 0); Require(openat_nid_postfix(base, "made.bin", SCE_KERNEL_O_WRONLY | SCE_KERNEL_O_CREAT | SCE_KERNEL_O_EXCL, 0644) == -1 && Error() == 17); Require(openat_nid_postfix(base, "missing.bin", SCE_KERNEL_O_RDONLY) == -1 && Error() == 2); + Require(openat_nid_postfix(base, "file.txt", SCE_KERNEL_O_ACCMODE) == -1 && Error() == 22); + Require(openat_nid_postfix(base, "absent.bin", SCE_KERNEL_O_WRONLY | SCE_KERNEL_O_CREAT | SCE_KERNEL_O_EXCL | SCE_KERNEL_O_DIRECTORY, 0644) == -1 && Error() == 22); + Require(openat_nid_postfix(base, nullptr, SCE_KERNEL_O_ACCMODE) == -1 && Error() == 22); + Require(openat_nid_postfix(base, "", SCE_KERNEL_O_ACCMODE) == -1 && Error() == 22); + Require(openat_nid_postfix(9999, "", SCE_KERNEL_O_WRONLY | SCE_KERNEL_O_CREAT | SCE_KERNEL_O_EXCL | SCE_KERNEL_O_DIRECTORY, 0644) == -1 && Error() == 22); + Require(openat_nid_postfix(-100, nullptr, SCE_KERNEL_O_ACCMODE) == -1 && Error() == 22); const auto guestFile = "/" + root.filename().string() + "/file.txt"; const int absolute = openat_nid_postfix(-1, guestFile.c_str(), SCE_KERNEL_O_RDONLY); Require(absolute >= 0 && sceKernelClose(absolute) == 0); @@ -488,6 +505,7 @@ int main() { Require(openat_nid_postfix(plain, "file.txt", SCE_KERNEL_O_RDONLY) == -1 && Error() == 20); Require(readlinkat_nid_postfix(base, "file.txt", buffer, sizeof(buffer)) == -1 && Error() == 22); Require(readlinkat_nid_postfix(base, "missing.bin", buffer, sizeof(buffer)) == -1 && Error() == 2); + Require(readlinkat_nid_postfix(base, "file.txt/child", buffer, sizeof(buffer)) == -1 && Error() == 20); Require(readlinkat_nid_postfix(plain, "file.txt", buffer, sizeof(buffer)) == -1 && Error() == 20); Require(readlinkat_nid_postfix(-1, "file.txt", buffer, sizeof(buffer)) == -1 && Error() == 9); Require(readlinkat_nid_postfix(base, nullptr, buffer, sizeof(buffer)) == -1 && Error() == 14); @@ -628,6 +646,29 @@ int main() { fchownThrew = false; try { fchown_nid_postfix(base, 0, 1); } catch (const std::exception&) { fchownThrew = true; } Require(fchownThrew); + FileStat stat{}; + Require(fstatat_nid_postfix(base, "file.txt", &stat, 0) == 0 && (stat.st_mode & 0170000) == 0100000 && stat.st_size == 1); + Require(fstatat_nid_postfix(base, "other_dir", &stat, 0) == 0 && (stat.st_mode & 0170000) == 0040000); + Require(fstatat_nid_postfix(base, "other_dir/", &stat, 0) == 0 && (stat.st_mode & 0170000) == 0040000); + Require(fstatat_nid_postfix(base, "file.txt/", &stat, 0) == -1 && Error() == 20); + Require(fstatat_nid_postfix(base, "file.txt", &stat, 0x0200) == 0 && (stat.st_mode & 0170000) == 0100000); + Require(fstatat_nid_postfix(-100, file.c_str(), &stat, 0) == 0 && stat.st_size == 1); + Require(fstatat_nid_postfix(-1, guestFile.c_str(), &stat, 0) == 0 && stat.st_size == 1); + Require(fstatat_nid_postfix(base, "missing.bin", &stat, 0) == -1 && Error() == 2); + Require(fstatat_nid_postfix(base, "missing.bin", &stat, 0x0200) == -1 && Error() == 2); + Require(fstatat_nid_postfix(base, nullptr, &stat, 0) == -1 && Error() == 14); + Require(fstatat_nid_postfix(base, "", &stat, 0) == -1 && Error() == 2); + Require(fstatat_nid_postfix(base, "file.txt", &stat, 0x4000) == -1 && Error() == 22); + Require(fstatat_nid_postfix(-1, "file.txt", &stat, 0) == -1 && Error() == 9); + const int notDirectory = sceKernelOpen(file.c_str(), SCE_KERNEL_O_RDONLY, 0); + Require(notDirectory >= 0); + Require(fstatat_nid_postfix(notDirectory, "file.txt", &stat, 0) == -1 && Error() == 20); + Require(sceKernelClose(notDirectory) == 0); +#ifndef _WIN32 + Require(symlink_nid_postfix("file.txt", (root / "link.txt").string().c_str()) == 0); + Require(fstatat_nid_postfix(base, "link.txt", &stat, 0) == 0 && (stat.st_mode & 0170000) == 0100000 && stat.st_size == 1); + Require(fstatat_nid_postfix(base, "link.txt", &stat, 0x0200) == 0 && (stat.st_mode & 0170000) == 0120000); +#endif Require(sceKernelClose(base) == 0); #ifdef _WIN32 const auto junctionTarget = root / "junction-target"; @@ -658,16 +699,6 @@ int main() { Require(!std::filesystem::exists(std::filesystem::symlink_status(throughLink))); } #endif -#ifdef _WIN32 - // std::filesystem::remove_all loops forever when the tree contains a - // dangling symbolic link: fs::remove reports such a link as missing and - // clears the error, so recursive_directory_iterator::__erase keeps - // retrying it. The fixture always contains dangling links here, so - // remove it through the shell instead. - const auto cleanup = L"cmd /c rd /s /q \"" + root.wstring() + L"\""; - Require(_wsystem(cleanup.c_str()) == 0); -#else - std::filesystem::remove_all(root); -#endif + RemoveTree(root); return 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/GuestKqueue.cpp b/core/libs/tests/GuestKqueue.cpp index 36b9dfb31c..2ff23394f4 100644 --- a/core/libs/tests/GuestKqueue.cpp +++ b/core/libs/tests/GuestKqueue.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -21,6 +22,7 @@ int APS5_VABI kevent_nid_postfix(int, const KernelEvent*, int, KernelEvent*, int int APS5_VABI close_nid_postfix(int); int APS5_VABI dup_nid_postfix(int); int APS5_VABI pipe_nid_postfix(int*); +int APS5_VABI fcntl_nid_postfix(int, int, ...); int APS5_VABI socketpair_nid_postfix(int, int, int, int*); 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); @@ -43,6 +45,11 @@ int* APS5_VABI __error_nid_postfix(); static constexpr int GuestEnoent = 2; static constexpr int GuestEbadf = 9; static constexpr int GuestEinval = 22; +static constexpr int GuestEpipe = 32; +static constexpr int GuestEagain = 35; +static constexpr int SetStatusFlags = 4; +static constexpr int NonBlocking = 0x0004; +static constexpr std::int64_t PipeAtomic = 512; static constexpr int Unix = 1; static constexpr int Stream = 1; static constexpr int FirstSocket = 0x10000000; @@ -253,6 +260,68 @@ static void PipeRead() { Require(close_nid_postfix(kq) == 0); } +#ifndef __APPLE__ +static void PipeWrite() { + const int kq = kqueue_nid_postfix(); + int pipe[2]; + Require(pipe_nid_postfix(pipe) == 0); + const auto writeEnd = static_cast(pipe[1]); + int marker = 0; + KernelEvent events[4]{}; + Require(Apply(kq, Change(writeEnd, EvfiltWrite, EvAdd, 0, 0, &marker)) == 0); + Require(Poll(kq, events, 4) == 1); + Require(events[0].ident == writeEnd && events[0].filter == EvfiltWrite && events[0].udata == &marker && (events[0].flags & EvEof) == 0); + const std::int64_t empty = events[0].data; + Require(empty >= PipeAtomic); + Require(write_nid_postfix(pipe[1], "abc", 3) == 3); + Require(Poll(kq, events, 4) == 1 && events[0].data == empty - 3); +#ifndef _WIN32 + Require(fcntl_nid_postfix(pipe[1], SetStatusFlags, NonBlocking) == 0); + const std::string chunk(4096, 'x'); + std::int64_t queued = 3; + for (;;) { + const std::int64_t written = write_nid_postfix(pipe[1], chunk.data(), static_cast(chunk.size())); + if (written < 0) { + Require(*__error_nid_postfix() == GuestEagain); + break; + } + queued += written; + } + Require(queued > PipeAtomic); + Require(Poll(kq, events, 4) == 0); + const KernelTimespec limit{5, 0}; + const auto start = std::chrono::steady_clock::now(); + std::thread reader([&pipe, queued] { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + std::string drained(static_cast(queued), '\0'); + std::int64_t done = 0; + while (done < queued) { + const std::int64_t got = read_nid_postfix(pipe[0], drained.data() + done, static_cast(queued - done)); + Require(got > 0); + done += got; + } + }); + Require(kevent_nid_postfix(kq, nullptr, 0, events, 4, &limit) == 1); + const auto waited = std::chrono::steady_clock::now() - start; + reader.join(); + Require(waited >= std::chrono::milliseconds(40) && waited < std::chrono::seconds(4)); + Require(events[0].ident == writeEnd && events[0].data >= PipeAtomic && (events[0].flags & EvEof) == 0); + Require(Poll(kq, events, 4) == 1 && events[0].data == empty); +#else + char back[3]; + Require(read_nid_postfix(pipe[0], back, 3) == 3); + Require(Poll(kq, events, 4) == 1 && events[0].data == empty); +#endif + Require(close_nid_postfix(pipe[0]) == 0); + Require(Poll(kq, events, 4) == 1 && events[0].data == 0 && (events[0].flags & EvEof) != 0); + const int late = kqueue_nid_postfix(); + RequireFailure(Apply(late, Change(writeEnd, EvfiltWrite, EvAdd)), GuestEpipe); + Require(close_nid_postfix(late) == 0); + Require(close_nid_postfix(pipe[1]) == 0); + Require(close_nid_postfix(kq) == 0); +} +#endif + static void SocketPair() { const int kq = kqueue_nid_postfix(); int pair[2]; @@ -271,7 +340,9 @@ static void SocketPair() { Require(Apply(kq, Change(static_cast(pair[1]), EvfiltWrite, EvAdd | EvOneshot)) == 0); Require(Wait(kq, events, 4) == 1 && events[0].filter == EvfiltWrite); RequireFailure(Apply(kq, Change(static_cast(pair[1]), EvfiltWrite, EvDelete)), GuestEnoent); +#ifndef __linux__ Require(Throws([kq, &pair] { Apply(kq, Change(static_cast(pair[1]), EvfiltRead, EvAdd | EvClear)); })); +#endif const int space = kqueue_nid_postfix(); Require(Apply(space, Change(static_cast(pair[1]), EvfiltWrite, EvAdd)) == 0); Require(Poll(space, events, 4) == 1 && events[0].filter == EvfiltWrite); @@ -315,7 +386,14 @@ static void LowWaterMarks() { Require(Poll(kq, events, 4) == 0); Require(Apply(kq, Change(static_cast(pair[1]), EvfiltWrite, EvAdd | EvOneshot, NoteLowat, 1)) == 0); Require(Wait(kq, events, 4) == 1 && events[0].filter == EvfiltWrite); - Require(Throws([kq, &pair] { Apply(kq, Change(static_cast(pair[1]), EvfiltWrite, EvAdd, NoteLowat, 2)); })); + const auto writer = static_cast(pair[1]); + Require(Apply(kq, Change(writer, EvfiltWrite, EvAdd, NoteLowat, 2)) == 0); + Require(Wait(kq, events, 4) == 1 && events[0].ident == writer && events[0].filter == EvfiltWrite && events[0].data >= 2); + const auto freeSpace = events[0].data; + Require(Apply(kq, Change(writer, EvfiltWrite, EvAdd, NoteLowat, freeSpace)) == 0); + Require(Poll(kq, events, 4) == 1 && events[0].ident == writer && events[0].data == freeSpace); + Require(Apply(kq, Change(writer, EvfiltWrite, EvAdd, NoteLowat, freeSpace + 1)) == 0); + Require(Poll(kq, events, 4) == 0); Require(Throws([kq, reader] { Apply(kq, Change(reader, EvfiltRead, EvAdd, NoteLowat, 0)); })); int datagrams[2]; Require(socketpair_nid_postfix(Unix, 2, 0, datagrams) == 0); @@ -338,6 +416,195 @@ static void LowWaterMarks() { Require(close_nid_postfix(kq) == 0); } +#ifdef __linux__ +static void EdgeTriggeredSocketRead() { + const int kq = kqueue_nid_postfix(); + int pair[2]; + Require(socketpair_nid_postfix(Unix, Stream, 0, pair) == 0); + const auto ident = static_cast(pair[0]); + int marker = 0; + KernelEvent events[4]{}; + char buffer[8]{}; + + Require(Apply(kq, Change(ident, EvfiltRead, EvAdd | EvClear, 0, 0, &marker)) == 0); + Require(Poll(kq, events, 4) == 0); + Require(send_nid_postfix(pair[1], "abc", 3, 0) == 3); + Require(Poll(kq, events, 4) == 1); + Require(events[0].ident == ident && events[0].filter == EvfiltRead && events[0].data == 3 && events[0].udata == &marker); + Require((events[0].flags & EvClear) != 0 && (events[0].flags & EvEof) == 0); + Require(Poll(kq, events, 4) == 0); + + const KernelTimespec quiet{0, 150000000}; + const auto idleStart = std::chrono::steady_clock::now(); + const std::clock_t idleCpu = std::clock(); + Require(kevent_nid_postfix(kq, nullptr, 0, events, 4, &quiet) == 0); + Require(std::clock() - idleCpu < CLOCKS_PER_SEC / 20); + Require(std::chrono::steady_clock::now() - idleStart >= std::chrono::milliseconds(140)); + + Require(send_nid_postfix(pair[1], "d", 1, 0) == 1); + Require(Poll(kq, events, 4) == 1 && events[0].data == 4); + Require(Poll(kq, events, 4) == 0); + + Require(Apply(kq, Change(ident, EvfiltRead, EvAdd | EvClear, 0, 0, &marker)) == 0); + Require(Poll(kq, events, 4) == 1 && events[0].data == 4); + Require(Poll(kq, events, 4) == 0); + + Require(recv_nid_postfix(pair[0], buffer, 2, 0) == 2); + Require(Poll(kq, events, 4) == 0); + Require(recv_nid_postfix(pair[0], buffer, sizeof(buffer), 0) == 2); + Require(send_nid_postfix(pair[1], "e", 1, 0) == 1); + Require(recv_nid_postfix(pair[0], buffer, sizeof(buffer), 0) == 1); + Require(Poll(kq, events, 4) == 0); + + const KernelTimespec limit{5, 0}; + const auto start = std::chrono::steady_clock::now(); + std::thread sender([&pair] { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + Require(send_nid_postfix(pair[1], "fg", 2, 0) == 2); + }); + Require(kevent_nid_postfix(kq, nullptr, 0, events, 4, &limit) == 1); + const auto waited = std::chrono::steady_clock::now() - start; + sender.join(); + Require(waited >= std::chrono::milliseconds(40) && waited < std::chrono::seconds(4)); + Require(events[0].ident == ident && events[0].data == 2); + + Require(close_nid_postfix(pair[1]) == 0); + Require(Poll(kq, events, 4) == 1 && events[0].data == 2 && (events[0].flags & EvEof) != 0); + Require(Poll(kq, events, 4) == 0); + Require(close_nid_postfix(pair[0]) == 0); + + int marked[2]; + Require(socketpair_nid_postfix(Unix, Stream, 0, marked) == 0); + const auto markedReader = static_cast(marked[0]); + Require(Apply(kq, Change(markedReader, EvfiltRead, EvAdd | EvClear, NoteLowat, 4)) == 0); + Require(send_nid_postfix(marked[1], "ab", 2, 0) == 2); + Require(Poll(kq, events, 4) == 0); + Require(send_nid_postfix(marked[1], "cd", 2, 0) == 2); + Require(Poll(kq, events, 4) == 1 && events[0].ident == markedReader && events[0].data == 4); + Require(Poll(kq, events, 4) == 0); + Require(close_nid_postfix(marked[0]) == 0 && close_nid_postfix(marked[1]) == 0); + Require(close_nid_postfix(kq) == 0); +} + +static void EdgeTriggeredSocketWrite() { + const int kq = kqueue_nid_postfix(); + int pair[2]; + Require(socketpair_nid_postfix(Unix, Stream, 0, pair) == 0); + const auto ident = static_cast(pair[1]); + KernelEvent events[4]{}; + Require(Apply(kq, Change(ident, EvfiltWrite, EvAdd | EvClear)) == 0); + Require(Poll(kq, events, 4) == 1 && events[0].ident == ident && events[0].filter == EvfiltWrite && events[0].data > 0); + Require(Poll(kq, events, 4) == 0); + const std::string payload(1000, 'w'); + Require(send_nid_postfix(pair[1], payload.data(), payload.size(), 0) == static_cast(payload.size())); + Require(Poll(kq, events, 4) == 0); + std::string drained(payload.size(), '\0'); + Require(recv_nid_postfix(pair[0], drained.data(), drained.size(), 0) == static_cast(drained.size())); + Require(Poll(kq, events, 4) == 1 && events[0].ident == ident && events[0].filter == EvfiltWrite && events[0].data > 0); + Require(Poll(kq, events, 4) == 0); + Require(Apply(kq, Change(ident, EvfiltWrite, EvAdd | EvClear)) == 0); + Require(Poll(kq, events, 4) == 1 && events[0].filter == EvfiltWrite); + Require(close_nid_postfix(pair[0]) == 0); + Require(Poll(kq, events, 4) == 1 && (events[0].flags & EvEof) != 0); + Require(close_nid_postfix(pair[1]) == 0); + Require(close_nid_postfix(kq) == 0); +} + +static void SocketWriteLowWaterWhileFilling() { + const int kq = kqueue_nid_postfix(); + int pair[2]; + Require(socketpair_nid_postfix(Unix, Stream, 0, pair) == 0); + const auto writer = static_cast(pair[1]); + KernelEvent events[4]{}; + Require(fcntl_nid_postfix(pair[1], SetStatusFlags, NonBlocking) == 0); + Require(Apply(kq, Change(writer, EvfiltWrite, EvAdd, NoteLowat, 2)) == 0); + Require(Poll(kq, events, 4) == 1); + const auto empty = events[0].data; + const std::string chunk(4096, 'x'); + std::int64_t queued = 0; + Require(send_nid_postfix(pair[1], chunk.data(), chunk.size(), 0) == static_cast(chunk.size())); + queued += static_cast(chunk.size()); + Require(Poll(kq, events, 4) == 1 && events[0].data > 0 && events[0].data < empty); + const auto partial = events[0].data; + Require(Apply(kq, Change(writer, EvfiltWrite, EvAdd, NoteLowat, partial)) == 0); + Require(Poll(kq, events, 4) == 1 && events[0].data == partial); + Require(Apply(kq, Change(writer, EvfiltWrite, EvAdd, NoteLowat, partial + 1)) == 0); + Require(Poll(kq, events, 4) == 0); + Require(Apply(kq, Change(writer, EvfiltWrite, EvAdd, NoteLowat, empty)) == 0); + Require(Poll(kq, events, 4) == 0); + for (;;) { + const std::int64_t written = send_nid_postfix(pair[1], chunk.data(), chunk.size(), 0); + if (written < 0) { + Require(*__error_nid_postfix() == GuestEagain); + break; + } + queued += written; + } + Require(Apply(kq, Change(writer, EvfiltWrite, EvAdd, NoteLowat, 2)) == 0); + Require(Poll(kq, events, 4) == 0); + std::string drained(chunk.size(), '\0'); + while (queued > 0) { + const std::int64_t received = recv_nid_postfix(pair[0], drained.data(), drained.size(), 0); + Require(received > 0); + queued -= received; + } + Require(Poll(kq, events, 4) == 1 && events[0].data == empty); + Require(close_nid_postfix(pair[0]) == 0 && close_nid_postfix(pair[1]) == 0); + Require(close_nid_postfix(kq) == 0); +} + +static void EdgeTriggeredPipeRead() { + const int kq = kqueue_nid_postfix(); + int pipe[2]; + Require(pipe_nid_postfix(pipe) == 0); + const auto ident = static_cast(pipe[0]); + KernelEvent events[4]{}; + char buffer[8]{}; + Require(Apply(kq, Change(ident, EvfiltRead, EvAdd | EvClear)) == 0); + Require(Poll(kq, events, 4) == 0); + Require(write_nid_postfix(pipe[1], "ab", 2) == 2); + Require(Poll(kq, events, 4) == 1 && events[0].ident == ident && events[0].data == 2 && (events[0].flags & EvEof) == 0); + Require(Poll(kq, events, 4) == 0); + Require(write_nid_postfix(pipe[1], "c", 1) == 1); + Require(Poll(kq, events, 4) == 1 && events[0].data == 3); + Require(Poll(kq, events, 4) == 0); + Require(read_nid_postfix(pipe[0], buffer, sizeof(buffer)) == 3); + Require(close_nid_postfix(pipe[1]) == 0); + Require(Poll(kq, events, 4) == 1 && events[0].data == 0 && (events[0].flags & EvEof) != 0); + Require(Poll(kq, events, 4) == 0); + Require(close_nid_postfix(pipe[0]) == 0); + Require(close_nid_postfix(kq) == 0); +} + +static void EdgeTriggeredDispatch() { + const int kq = kqueue_nid_postfix(); + int pair[2]; + Require(socketpair_nid_postfix(Unix, Stream, 0, pair) == 0); + const auto ident = static_cast(pair[0]); + KernelEvent events[4]{}; + char buffer[8]{}; + Require(Apply(kq, Change(ident, EvfiltRead, EvAdd | EvClear | EvDispatch)) == 0); + Require(send_nid_postfix(pair[1], "ab", 2, 0) == 2); + Require(Poll(kq, events, 4) == 1 && events[0].data == 2); + Require(send_nid_postfix(pair[1], "c", 1, 0) == 1); + Require(Poll(kq, events, 4) == 0); + Require(Apply(kq, Change(ident, EvfiltRead, EvEnable)) == 0); + Require(Poll(kq, events, 4) == 1 && events[0].data == 3); + Require(Poll(kq, events, 4) == 0); + Require(recv_nid_postfix(pair[0], buffer, sizeof(buffer), 0) == 3); + Require(Apply(kq, Change(ident, EvfiltRead, EvEnable)) == 0); + Require(Poll(kq, events, 4) == 0); + Require(Apply(kq, Change(ident, EvfiltRead, EvDisable)) == 0); + Require(send_nid_postfix(pair[1], "d", 1, 0) == 1); + Require(Poll(kq, events, 4) == 0); + Require(Apply(kq, Change(ident, EvfiltRead, EvEnable)) == 0); + Require(Poll(kq, events, 4) == 1 && events[0].data == 1); + Require(Apply(kq, Change(ident, EvfiltRead, EvDelete)) == 0); + Require(close_nid_postfix(pair[0]) == 0 && close_nid_postfix(pair[1]) == 0); + Require(close_nid_postfix(kq) == 0); +} +#endif + static void DisableAndDispatch() { const int kq = kqueue_nid_postfix(); int pair[2]; @@ -412,8 +679,13 @@ static void Errors() { int pipe[2]; Require(pipe_nid_postfix(pipe) == 0); Require(Throws([kq, &pipe] { Apply(kq, Change(static_cast(pipe[1]), EvfiltRead, EvAdd)); })); + Require(Throws([kq, &pipe] { Apply(kq, Change(static_cast(pipe[0]), EvfiltWrite, EvAdd)); })); +#ifdef __APPLE__ Require(Throws([kq, &pipe] { Apply(kq, Change(static_cast(pipe[1]), EvfiltWrite, EvAdd)); })); +#endif +#ifndef __linux__ Require(Throws([kq, &pipe] { Apply(kq, Change(static_cast(pipe[0]), EvfiltRead, EvAdd | EvClear)); })); +#endif Require(close_nid_postfix(pipe[0]) == 0 && close_nid_postfix(pipe[1]) == 0); const KernelEvent skipped = Change(1, 0, EvAdd); const KernelTimespec zero{0, 0}; @@ -727,7 +999,7 @@ static void StandardInput() { Require(Apply(kq, Change(0, EvfiltRead, EvAdd, 0, 0, &marker)) == 0); Require(Apply(kq, Change(0, EvfiltRead, EvDelete)) == 0); RequireFailure(Apply(kq, Change(0, EvfiltRead, EvDelete)), GuestEnoent); - Require(ThrownMessage([kq] { Apply(kq, Change(0, EvfiltWrite, EvAdd)); }).find("EVFILT_WRITE on a non-socket descriptor") != std::string::npos); + Require(ThrownMessage([kq] { Apply(kq, Change(0, EvfiltWrite, EvAdd)); }).find("EVFILT_WRITE on a descriptor that is neither a socket nor the write end of a pipe") != std::string::npos); Require(close_nid_postfix(kq) == 0); } @@ -736,8 +1008,18 @@ int main() { CreateAndClose(); Timers(); PipeRead(); +#ifndef __APPLE__ + PipeWrite(); +#endif UserEvents(); SocketPair(); +#ifdef __linux__ + EdgeTriggeredSocketRead(); + EdgeTriggeredSocketWrite(); + SocketWriteLowWaterWhileFilling(); + EdgeTriggeredPipeRead(); + EdgeTriggeredDispatch(); +#endif DisableAndDispatch(); Errors(); LowWaterMarks(); 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/GuestLibcThreads.cpp b/core/libs/tests/GuestLibcThreads.cpp index 0aabfe21ce..ac1015b41f 100644 --- a/core/libs/tests/GuestLibcThreads.cpp +++ b/core/libs/tests/GuestLibcThreads.cpp @@ -17,6 +17,23 @@ 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_signal_nid_postfix(void** handle); + +struct GuestPad { + unsigned (APS5_VABI* const* vtable)(GuestPad*); + void* condition; + void* mutex; + bool started; +}; + +void APS5_VABI _ZNSt4_PadC2Ev_nid_postfix(GuestPad* pad); +void APS5_VABI _ZNSt4_PadD2Ev_nid_postfix(GuestPad* pad); +void APS5_VABI _ZNSt4_Pad8_ReleaseEv_nid_postfix(GuestPad* pad); +void APS5_VABI _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(GuestPad* pad, void** thread); +int APS5_VABI _Thrd_join_nid_postfix(void* thread, int* code); +int APS5_VABI _Thrd_detach_nid_postfix(void* thread); +void RegisterPthreadCreate_nid_no_patch(int (APS5_VABI* fn)(void**, const void*, void* (APS5_VABI*)(void*), void*)); +void RegisterPthreadJoin_nid_no_patch(int (APS5_VABI* fn)(void*, void**)); +void RegisterPthreadDetach_nid_no_patch(int (APS5_VABI* fn)(void*)); } static void Require(bool value) { if (!value) std::abort(); } @@ -183,10 +200,111 @@ static void ConditionSignal() { _Cnd_destroy_nid_postfix(&condition); } +struct TestPad { + unsigned (APS5_VABI* const* vtable)(GuestPad*); + void* condition; + void* mutex; + bool started; + std::atomic ran{false}; +}; + +struct HostPadThread { + std::thread worker; + void* result = nullptr; +}; + +static int APS5_VABI HostPthreadCreate(void** thread, const void*, void* (APS5_VABI* entry)(void*), void* arg) { + auto* handle = new HostPadThread{}; + handle->worker = std::thread([handle, entry, arg] { handle->result = entry(arg); }); + *thread = handle; + return 0; +} + +static int APS5_VABI HostPthreadJoin(void* thread, void** value) { + if (!thread) return 22; + auto* handle = static_cast(thread); + handle->worker.join(); + if (value) *value = handle->result; + delete handle; + return 0; +} + +static int APS5_VABI HostPthreadDetach(void* thread) { + if (!thread) return 22; + static_cast(thread)->worker.detach(); + return 0; +} + +static unsigned APS5_VABI TestPadGo(GuestPad* base) { + auto* pad = reinterpret_cast(base); + pad->ran.store(true); + _ZNSt4_Pad8_ReleaseEv_nid_postfix(base); + return 42; +} + +static void PadLaunchAndJoin() { + unsigned (APS5_VABI* vtable[1])(GuestPad*) = {TestPadGo}; + TestPad unregPad{}; + unregPad.vtable = vtable; + _ZNSt4_PadC2Ev_nid_postfix(reinterpret_cast(&unregPad)); + RegisterPthreadCreate_nid_no_patch(nullptr); + RegisterPthreadJoin_nid_no_patch(nullptr); + void* unregThread = nullptr; + RequireThrows([&] { + _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(reinterpret_cast(&unregPad), &unregThread); + }); + RequireThrows([&] { + int code = 0; + _Thrd_join_nid_postfix(&unregPad, &code); + }); + _ZNSt4_PadD2Ev_nid_postfix(reinterpret_cast(&unregPad)); + + RegisterPthreadCreate_nid_no_patch(HostPthreadCreate); + RegisterPthreadJoin_nid_no_patch(HostPthreadJoin); + + TestPad pad{}; + pad.vtable = vtable; + _ZNSt4_PadC2Ev_nid_postfix(reinterpret_cast(&pad)); + void* thread = nullptr; + _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(reinterpret_cast(&pad), &thread); + Require(thread != nullptr && pad.started && pad.ran.load()); + _ZNSt4_PadD2Ev_nid_postfix(reinterpret_cast(&pad)); + int code = 0; + Require(_Thrd_join_nid_postfix(thread, &code) == success && code == 42); + Require(_Thrd_join_nid_postfix(nullptr, &code) == 4); + + RegisterPthreadCreate_nid_no_patch(nullptr); + RegisterPthreadJoin_nid_no_patch(nullptr); +} + +static void PadDetach() { + unsigned (APS5_VABI* vtable[1])(GuestPad*) = {TestPadGo}; + RegisterPthreadDetach_nid_no_patch(nullptr); + RequireThrows([] { _Thrd_detach_nid_postfix(nullptr); }); + + RegisterPthreadCreate_nid_no_patch(HostPthreadCreate); + RegisterPthreadDetach_nid_no_patch(HostPthreadDetach); + + TestPad pad{}; + pad.vtable = vtable; + _ZNSt4_PadC2Ev_nid_postfix(reinterpret_cast(&pad)); + void* thread = nullptr; + _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(reinterpret_cast(&pad), &thread); + Require(thread != nullptr && pad.ran.load()); + _ZNSt4_PadD2Ev_nid_postfix(reinterpret_cast(&pad)); + Require(_Thrd_detach_nid_postfix(thread) == success); + Require(_Thrd_detach_nid_postfix(nullptr) == 4); + + RegisterPthreadCreate_nid_no_patch(nullptr); + RegisterPthreadDetach_nid_no_patch(nullptr); +} + int main() { MutexBasics(); RecursiveMutex(); MutualExclusion(); ConditionBroadcast(); ConditionSignal(); + PadLaunchAndJoin(); + PadDetach(); } diff --git a/core/libs/tests/GuestLocale.cpp b/core/libs/tests/GuestLocale.cpp index a72216251b..71b71a1669 100644 --- a/core/libs/tests/GuestLocale.cpp +++ b/core/libs/tests/GuestLocale.cpp @@ -24,6 +24,7 @@ struct StreamManipulator { std::int32_t argument; }; StreamManipulator APS5_VABI _ZSt4setwi_nid_postfix(std::int32_t width); +void APS5_VABI _ZNSt8ios_base7_AddstdEPS__nid_postfix(GuestLocale::IosBase* self); const short* APS5_VABI _Getpctype_nid_postfix(); const short* APS5_VABI _Getptolower_nid_postfix(); const short* APS5_VABI _Getptoupper_nid_postfix(); @@ -176,6 +177,47 @@ static void CheckStreamWidth() { RequireException([&] { manipulator.function(nullptr, manipulator.argument); }); } +static void CheckStandardStreams() { + struct GuardedStream { + std::uint64_t before = 0x12345678; + GuestLocale::IosBase stream{}; + std::uint64_t after = 0x87654321; + } streams[9]; + auto* classic = &_ZSt21_sceLibcClassicLocale_nid_postfix; + for (auto& object : streams) object.stream.locale = classic; + RequireException([&] { _ZNSt8ios_base7_AddstdEPS__nid_postfix(nullptr); }); + _ZNSt8ios_base7_AddstdEPS__nid_postfix(&streams[0].stream); + _ZNSt8ios_base7_AddstdEPS__nid_postfix(&streams[1].stream); + Require(streams[0].stream.standardStream == 1 && streams[1].stream.standardStream == 2); + _ZNSt8ios_base7_AddstdEPS__nid_postfix(&streams[0].stream); + Require(streams[0].stream.standardStream == 1); + _ZNSt8ios_baseD2Ev_nid_postfix(&streams[0].stream); + Require(streams[0].stream.locale == classic && streams[0].stream.standardStream == 1); + _ZNSt8ios_baseD2Ev_nid_postfix(&streams[0].stream); + Require(streams[0].stream.locale == nullptr && streams[0].stream.standardStream == 1); + streams[0].stream.locale = classic; + _ZNSt8ios_base7_AddstdEPS__nid_postfix(&streams[0].stream); + Require(streams[0].stream.standardStream == 1); + _ZNSt8ios_baseD2Ev_nid_postfix(&streams[1].stream); + Require(streams[1].stream.locale == nullptr && streams[1].stream.standardStream == 2); + for (int index = 2; index < 7; ++index) { + _ZNSt8ios_base7_AddstdEPS__nid_postfix(&streams[index].stream); + Require(streams[index].stream.standardStream == index + 1); + } + _ZNSt8ios_base7_AddstdEPS__nid_postfix(&streams[7].stream); + _ZNSt8ios_base7_AddstdEPS__nid_postfix(&streams[8].stream); + Require(streams[7].stream.standardStream == 8 && streams[8].stream.standardStream == 8); + _ZNSt8ios_baseD2Ev_nid_postfix(&streams[7].stream); + Require(streams[7].stream.locale == classic); + _ZNSt8ios_baseD2Ev_nid_postfix(&streams[8].stream); + Require(streams[8].stream.locale == nullptr); + for (int index = 2; index < 7; ++index) { + _ZNSt8ios_baseD2Ev_nid_postfix(&streams[index].stream); + Require(streams[index].stream.locale == nullptr && streams[index].stream.standardStream == index + 1); + } + for (auto& object : streams) Require(object.before == 0x12345678 && object.after == 0x87654321); +} + static void CheckWideCaseTranslation() { const auto* lower = _Getptolower_nid_postfix(); const auto* upper = _Getptoupper_nid_postfix(); @@ -205,6 +247,7 @@ int main() { CheckCharacterConversions(); CheckStreamDestruction(); CheckStreamWidth(); + CheckStandardStreams(); CheckWideCaseTranslation(); _Locksyslock_nid_postfix(); _Locksyslock_nid_postfix(); diff --git a/core/libs/tests/GuestLoginDialog.cpp b/core/libs/tests/GuestLoginDialog.cpp index cd631caa4e..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,20 +61,34 @@ 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); Require(sceLoginDialogTerminate() == kErrNotInitialized); Require(sceLoginDialogGetStatus() == kStatusNone); + + Require(sceLoginDialogInitialize() == 0); + Require(sceLoginDialogGetStatus() == kStatusInitialized); + Require(sceLoginDialogClose() == 0); + Require(sceLoginDialogGetStatus() == kStatusFinished); + result[0] = -1; + Require(sceLoginDialogGetResult(result) == 0); + Require(result[0] == kResultUserCanceled); + Require(sceLoginDialogTerminate() == 0); } diff --git a/core/libs/tests/GuestM4aacEnc.cpp b/core/libs/tests/GuestM4aacEnc.cpp new file mode 100644 index 0000000000..d9f6f00888 --- /dev/null +++ b/core/libs/tests/GuestM4aacEnc.cpp @@ -0,0 +1,66 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +extern "C" { +int32_t APS5_VABI sceM4aacEncClearContext(int32_t m4aacEncHandle); +int APS5_VABI sceM4aacEncClearContextEx(); +int32_t APS5_VABI sceM4aacEncCreateEncoder(int32_t channels, int32_t samplingRate, int32_t bitRate, int32_t inputFormat, int32_t outputFormat, int32_t* pInternalError); +int APS5_VABI sceM4aacEncCreateEncoderEx(); +int32_t APS5_VABI sceM4aacEncDeleteEncoder(int32_t m4aacEncHandle, int32_t* pInternalError); +int APS5_VABI sceM4aacEncDeleteEncoderEx(); +int32_t APS5_VABI sceM4aacEncEncode(int32_t m4aacEncHandle, void* pInput, uint32_t inputByte, void* pOutput, uint32_t* pOutputByte, int32_t* pInternalError); +int APS5_VABI sceM4aacEncEncodeEx(); +int APS5_VABI sceM4aacEncEncodeWithPriority(); +int APS5_VABI sceM4aacEncEncodeWithPriorityEx(); +int32_t APS5_VABI sceM4aacEncFlush(int32_t m4aacEncHandle, void* pOutput, uint32_t* pOutputByte, int32_t* pInternalError); +int APS5_VABI sceM4aacEncFlushEx(); +int APS5_VABI sceM4aacEncFlushWithPriority(); +int APS5_VABI sceM4aacEncFlushWithPriorityEx(); +int APS5_VABI sceM4aacEncGetContext(); +int APS5_VABI sceM4aacEncInitialize(); +int APS5_VABI sceM4aacEncTerminate(); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +template +bool Throws(Call call) { + try { + static_cast(call()); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + +} + +int main() { + uint32_t outputByte = 0; + int32_t internalError = 0; + + Require(Throws([&] { return sceM4aacEncClearContext(1); })); + Require(Throws([&] { return sceM4aacEncClearContextEx(); })); + Require(Throws([&] { return sceM4aacEncCreateEncoder(2, 48000, 128000, 0, 0, &internalError); })); + Require(Throws([&] { return sceM4aacEncCreateEncoderEx(); })); + Require(Throws([&] { return sceM4aacEncDeleteEncoder(1, &internalError); })); + Require(Throws([&] { return sceM4aacEncDeleteEncoderEx(); })); + Require(Throws([&] { return sceM4aacEncEncode(1, nullptr, 0, nullptr, &outputByte, &internalError); })); + Require(Throws([&] { return sceM4aacEncEncodeEx(); })); + Require(Throws([&] { return sceM4aacEncEncodeWithPriority(); })); + Require(Throws([&] { return sceM4aacEncEncodeWithPriorityEx(); })); + Require(Throws([&] { return sceM4aacEncFlush(1, nullptr, &outputByte, &internalError); })); + Require(Throws([&] { return sceM4aacEncFlushEx(); })); + Require(Throws([&] { return sceM4aacEncFlushWithPriority(); })); + Require(Throws([&] { return sceM4aacEncFlushWithPriorityEx(); })); + Require(Throws([&] { return sceM4aacEncGetContext(); })); + Require(Throws([&] { return sceM4aacEncInitialize(); })); + Require(Throws([&] { return sceM4aacEncTerminate(); })); + + Require(outputByte == 0); + Require(internalError == 0); +} diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 176ac81a25..d7a52ec814 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -2,8 +2,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -36,6 +38,7 @@ unsigned long long APS5_VABI strtoull_nid_postfix(const char*, char**, int); std::uintmax_t APS5_VABI strtoumax_nid_postfix(const char*, char**, int); unsigned long long APS5_VABI _Stoull_nid_postfix(const char*, char**, int); std::uint64_t APS5_VABI _Stoul_nid_postfix(const char*, char**, int); +int APS5_VABI atoi_nid_postfix(const char*); std::int64_t APS5_VABI atol_nid_postfix(const char*); long long APS5_VABI atoll_nid_postfix(const char*); int* APS5_VABI __error_nid_postfix(); @@ -57,6 +60,18 @@ long double APS5_VABI frexpl_nid_postfix(long double, int*); struct LibcFloatConstant { std::uint32_t bits[4]; }; extern LibcFloatConstant _FInf_nid_postfix; extern LibcFloatConstant _FNan_nid_postfix; +extern LibcFloatConstant _FDenorm_nid_postfix; +extern LibcFloatConstant _FSnan_nid_postfix; +union SonyDconst { unsigned short _Word[8]; float _Float; double _Double; long double _Long_double; }; +extern SonyDconst _Inf_nid_postfix; +extern SonyDconst _Denorm_nid_postfix; +extern SonyDconst _Hugeval_nid_postfix; +extern SonyDconst _Nan_nid_postfix; +extern SonyDconst _Snan_nid_postfix; +extern SonyDconst _LDenorm_nid_postfix; +extern SonyDconst _LInf_nid_postfix; +extern SonyDconst _LNan_nid_postfix; +extern SonyDconst _LSnan_nid_postfix; short APS5_VABI _FDtest_nid_postfix(const float*); short APS5_VABI _Dtest_nid_postfix(const double*); int APS5_VABI __fpclassifyf_nid_postfix(float); @@ -107,6 +122,7 @@ double APS5_VABI ldexp_nid_postfix(double, int); double APS5_VABI scalbn_nid_postfix(double, int); float APS5_VABI scalbnf_nid_postfix(float, int); float APS5_VABI nextafterf_nid_postfix(float, float); +double APS5_VABI nextafter_nid_postfix(double, double); double APS5_VABI frexp_nid_postfix(double, int*); float APS5_VABI frexpf_nid_postfix(float, int*); std::int64_t APS5_VABI lround_nid_postfix(double); @@ -115,6 +131,8 @@ double APS5_VABI sinh_nid_postfix(double); double APS5_VABI cosh_nid_postfix(double); float APS5_VABI coshf_nid_postfix(float); float APS5_VABI sinhf_nid_postfix(float); +float APS5_VABI _FSinh_nid_postfix(float, float); +float APS5_VABI _FCosh_nid_postfix(float, float); double APS5_VABI asinh_nid_postfix(double); float APS5_VABI asinhf_nid_postfix(float); double APS5_VABI acosh_nid_postfix(double); @@ -140,6 +158,7 @@ std::int64_t APS5_VABI llabs_nid_postfix(std::int64_t); std::int64_t APS5_VABI imaxabs_nid_postfix(std::int64_t); long double APS5_VABI scalbnl_nid_postfix(long double, int); float APS5_VABI significandf_nid_postfix(float); +double APS5_VABI significand_nid_postfix(double); long double APS5_VABI acoshl_nid_postfix(long double); long double APS5_VABI asinhl_nid_postfix(long double); long double APS5_VABI atanhl_nid_postfix(long double); @@ -152,6 +171,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); @@ -182,6 +202,20 @@ long double APS5_VABI truncl_nid_postfix(long double); long double APS5_VABI nearbyintl_nid_postfix(long double); double APS5_VABI logb_nid_postfix(double); double APS5_VABI remainder_nid_postfix(double, double); +struct DinkumwareFloatCharacteristics { + int digits; + int mantissaDigits; + int maxExponent10; + int maxExponent; + int minExponent10; + int minExponent; + alignas(16) unsigned char epsilon[16]; + alignas(16) unsigned char max[16]; + alignas(16) unsigned char min[16]; +}; +extern DinkumwareFloatCharacteristics _Flt_nid_postfix; +extern DinkumwareFloatCharacteristics _Dbl_nid_postfix; +extern DinkumwareFloatCharacteristics _Ldbl_nid_postfix; } static void Require(bool value) { if (!value) std::abort(); } @@ -315,42 +349,152 @@ 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 CheckFloatUnderflowErrno() { + char* end = nullptr; + errno = 7; + Require(strtod_nid_postfix("1e-310", &end) == 1e-310 && errno == 7); + Require(strtod_nid_postfix("-4.9406564584124654e-324", &end) == -0x1p-1074 && errno == 7); + Require(strtof_nid_postfix("1e-40", &end) == 1e-40f && errno == 7); + Require(strtold_nid_postfix("1e-4940", &end) == 1e-4940L && errno == 7); + Require(strtod_nid_postfix("1e-330", &end) == 0.0 && errno == ERANGE); + errno = 0; + Require(strtof_nid_postfix("1e-46", &end) == 0.0f && errno == ERANGE); + errno = 0; + Require(strtod_nid_postfix("1e400", &end) == HUGE_VAL && errno == ERANGE); + errno = 0; + Require(strtod_nid_postfix("2.2250738585072014e-308", &end) == 0x1p-1022 && errno == 0); } static void CheckHypotSpecialValue() { @@ -517,9 +661,9 @@ static void CheckIntegerConversions() { {" -0B1!", 0, 0, 3, 0}, {"0b2", 0, 0, 1, 0}, {"0b101", 16, 0xb101, 5, 0}, - {" -0xz!", 0, 0, 3, 0}, - {"0xg", 16, 0, 1, 0}, - {"0x", 16, 0, 1, 0}, + {" -0xz!", 0, 0, 0, 0}, + {"0xg", 16, 0, 0, 0}, + {"0x", 16, 0, 0, 0}, {"z!", 36, 35, 1, 0}, {"", 10, 0, 0, 0}, {" \t+!", 10, 0, 0, 0}, @@ -565,8 +709,8 @@ static void CheckIntegerConversions() { {"0B11", 2, 0, 1, 0}, {" +0b1!", 2, 0, 3, 0}, {"0b101", 16, 0xb101, 5, 0}, - {" +0Xg!", 0, 0, 3, 0}, - {"0x!", 16, 0, 1, 0}, + {" +0Xg!", 0, 0, 0, 0}, + {"0x!", 16, 0, 0, 0}, {"z!", 36, 35, 1, 0}, {"", 10, 0, 0, 0}, {" \t-!", 10, 0, 0, 0}, @@ -604,6 +748,26 @@ static void CheckIntegerConversions() { Require(*__error_nid_postfix() == 13); Require(_Stoul_nid_postfix("18446744073709551615", nullptr, 10) == UINT64_MAX); Require(_Stoul_nid_postfix("-1", nullptr, 10) == UINT64_MAX && *__error_nid_postfix() == 13); + for (const int base : {-1, 1, 37}) { + const char text[] = " 12"; + const auto rejects = [&](auto parse, int error) { + char* end = nullptr; + *__error_nid_postfix() = 0; + Require(parse(text, &end, base) == 0 && end == text && *__error_nid_postfix() == error); + }; + rejects(strtol_nid_postfix, EINVAL); + rejects(strtoul_nid_postfix, EINVAL); + rejects(strtoll_nid_postfix, EINVAL); + rejects(strtoull_nid_postfix, EINVAL); + rejects(strtoimax_nid_postfix, 0); + rejects(strtoumax_nid_postfix, 0); + rejects(_Stoull_nid_postfix, 0); + rejects(_Stoul_nid_postfix, 0); + } + *__error_nid_postfix() = 0; + Require(atoi_nid_postfix("9223372036854775808") == 0 && atoi_nid_postfix("4294967297") == 1 && atoi_nid_postfix("-9223372036854775809") == -1); + Require(atol_nid_postfix("9223372036854775808") == INT64_MIN && atoll_nid_postfix("-9223372036854775809") == INT64_MAX && *__error_nid_postfix() == 0); + Require(atol_nid_postfix("99999999999999999999") == -1 && *__error_nid_postfix() == ERANGE); *__error_nid_postfix() = 0; } @@ -695,6 +859,98 @@ static void CheckHyperbolic() { Require(Near(acosh_nid_postfix(1e300), 691.4686750787736, 1e-15)); } +static void CheckScaledHyperbolic() { + const int rounding = std::fegetround(); + Require(std::fesetround(FE_TONEAREST) == 0); + unsigned checks = 0; + for (bool sine : {true, false}) { + const auto function = sine ? _FSinh_nid_postfix : _FCosh_nid_postfix; + const char* name = sine ? "_FSinh" : "_FCosh"; + const auto checkValue = [&](float x, float y) { + const long double value = sine ? std::sinh(static_cast(x)) : std::cosh(static_cast(x)); + const float expected = static_cast(value * static_cast(y)); + std::feclearexcept(FE_ALL_EXCEPT); + errno = 12345; + const float result = function(x, y); + const auto actualBits = std::bit_cast(result); + const auto expectedBits = std::bit_cast(expected); + const auto distance = actualBits > expectedBits ? actualBits - expectedBits : expectedBits - actualBits; + const bool exact = expected == 0.0f || std::isinf(expected); + if ((exact ? actualBits != expectedBits : !std::isfinite(result) || distance > 2) || + (std::isfinite(expected) && (std::fetestexcept(FE_OVERFLOW | FE_INVALID) != 0 || errno != 12345))) { + std::fprintf(stderr, "%s(%a, %a): got %08x, expected %08x, errno %d, flags %x\n", + name, static_cast(x), static_cast(y), actualBits, expectedBits, errno, std::fetestexcept(FE_ALL_EXCEPT)); + std::abort(); + } + ++checks; + }; + checkValue(90.0f, 1e-20f); + checkValue(100.0f, 1e-40f); + const float magnitudes[] = {0.0f, 0x1p-126f, 0.001f, 0.5f, 1.0f, 4.0f, 7.0f, 8.0f, 10.0f, 20.0f, 50.0f, 88.0f, 90.0f, 100.0f, 150.0f, 180.0f, 190.0f, 193.0f}; + const float scales[] = {0.0f, 0x1p-149f, 0x1p-140f, 0x1p-126f, 1e-20f, 0.5f, 1.0f, 1e20f, std::numeric_limits::max()}; + for (float magnitude : magnitudes) { + for (float scale : scales) { + for (float xSign : {1.0f, -1.0f}) { + for (float ySign : {1.0f, -1.0f}) checkValue(magnitude * xSign, scale * ySign); + } + } + } + for (float scale : scales) { + if (scale == 0.0f) continue; + for (long double target : {static_cast(std::numeric_limits::max()), 0x1p-150L}) { + if (!sine && target < scale) continue; + const long double ratio = target / static_cast(scale); + const float boundary = static_cast(sine ? std::asinh(ratio) : std::acosh(ratio)); + float magnitude = std::nextafter(std::nextafter(boundary, -INFINITY), -INFINITY); + for (unsigned neighbor = 0; neighbor != 5; ++neighbor) { + for (float xSign : {1.0f, -1.0f}) { + for (float ySign : {1.0f, -1.0f}) checkValue(magnitude * xSign, scale * ySign); + } + magnitude = std::nextafter(magnitude, INFINITY); + } + } + } + const auto checkSpecial = [&](float x, float y, std::uint32_t expectedBits, int error, int flags) { + std::feclearexcept(FE_ALL_EXCEPT); + std::feraiseexcept(FE_DIVBYZERO); + errno = 12345; + const float result = function(x, y); + const int raised = std::fetestexcept(FE_ALL_EXCEPT); + if (std::bit_cast(result) != expectedBits || errno != error || raised != (FE_DIVBYZERO | flags)) { + std::fprintf(stderr, "%s special (%a, %a): got %08x, expected %08x, errno %d, flags %x\n", + name, static_cast(x), static_cast(y), std::bit_cast(result), expectedBits, errno, raised); + std::abort(); + } + ++checks; + }; + const float infinity = std::numeric_limits::infinity(); + for (float sign : {1.0f, -1.0f}) { + checkSpecial(sign * infinity, 0.0f, 0x7fc00000u, EDOM, FE_INVALID); + checkSpecial(sign * 100.0f, 0.0f, sine && sign < 0 ? 0x80000000u : 0u, 12345, 0); + checkSpecial(sign * 100.0f, -0.0f, sine && sign < 0 ? 0u : 0x80000000u, 12345, 0); + checkSpecial(sign * 1.0f, infinity, sine && sign < 0 ? 0xff800000u : 0x7f800000u, 12345, 0); + } + for (std::uint32_t bits : {0x7fc12345u, 0xffc54321u, 0x7fa12345u, 0xffa54321u}) { + const float nan = std::bit_cast(bits); + checkSpecial(nan, 0.0f, bits, 12345, 0); + checkSpecial(infinity, nan, bits, 12345, 0); + checkSpecial(nan, std::bit_cast(0x7fc99999u), bits, 12345, 0); + } + checkSpecial(0.0f, infinity, sine ? 0x7fc00000u : 0x7f800000u, sine ? EDOM : 12345, sine ? FE_INVALID : 0); + checkSpecial(1.0f, std::numeric_limits::max(), 0x7f800000u, 12345, FE_OVERFLOW | FE_INEXACT); + checkSpecial(100.0f, 1.0f, 0x7f800000u, ERANGE, FE_OVERFLOW | FE_INEXACT); + for (int mode : {FE_TONEAREST, FE_DOWNWARD, FE_UPWARD, FE_TOWARDZERO}) { + Require(std::fesetround(mode) == 0); + checkSpecial(std::numeric_limits::max(), 0x1p-149f, 0x7f800000u, ERANGE, FE_OVERFLOW | FE_INEXACT); + } + Require(std::fesetround(FE_TONEAREST) == 0); + if (sine) checkSpecial(0x1p-126f, 0x1p-149f, 0u, 12345, FE_UNDERFLOW | FE_INEXACT); + } + std::feclearexcept(FE_ALL_EXCEPT); + Require(std::fesetround(rounding) == 0); + std::printf("Scaled hyperbolic checks: %u\n", checks); +} + static void CheckSinhf() { const float infinity = std::numeric_limits::infinity(); const struct { std::uint32_t input; std::uint32_t output; } cases[] = { @@ -785,6 +1041,36 @@ static void CheckFloatClassification() { Require(_FDtest_nid_postfix(reinterpret_cast(&_FNan_nid_postfix)) == 2); } +static void RequireBytes(const void* constant, std::initializer_list expected) { + unsigned char bytes[16]; + std::memcpy(bytes, constant, sizeof(bytes)); + int index = 0; + for (const unsigned char byte : expected) Require(bytes[index++] == byte); + while (index < 16) Require(bytes[index++] == 0); +} + +static void CheckDinkumwareConstants() { + RequireBytes(&_Inf_nid_postfix, {0, 0, 0, 0, 0, 0, 0xf0, 0x7f}); + RequireBytes(&_Denorm_nid_postfix, {1}); + RequireBytes(&_Hugeval_nid_postfix, {0, 0, 0, 0, 0, 0, 0xf0, 0x7f}); + RequireBytes(&_Nan_nid_postfix, {0, 0, 0, 0, 0, 0, 0xf8, 0x7f}); + RequireBytes(&_Snan_nid_postfix, {1, 0, 0, 0, 0, 0, 0xf0, 0x7f}); + RequireBytes(&_FDenorm_nid_postfix, {1}); + RequireBytes(&_FSnan_nid_postfix, {1, 0, 0x80, 0x7f}); + RequireBytes(&_LDenorm_nid_postfix, {1}); + RequireBytes(&_LInf_nid_postfix, {0, 0, 0, 0, 0, 0, 0, 0x80, 0xff, 0x7f}); + RequireBytes(&_LNan_nid_postfix, {0, 0, 0, 0, 0, 0, 0, 0xc0, 0xff, 0x7f}); + RequireBytes(&_LSnan_nid_postfix, {1, 0, 0, 0, 0, 0, 0, 0x80, 0xff, 0x7f}); + Require(_Dtest_nid_postfix(&_Denorm_nid_postfix._Double) == -2 && _Denorm_nid_postfix._Double == std::numeric_limits::denorm_min()); + Require(_Dtest_nid_postfix(&_Hugeval_nid_postfix._Double) == 1 && _Hugeval_nid_postfix._Double == HUGE_VAL); + Require(_Dtest_nid_postfix(&_Nan_nid_postfix._Double) == 2 && _Dtest_nid_postfix(&_Snan_nid_postfix._Double) == 2); + Require(_FDtest_nid_postfix(reinterpret_cast(&_FDenorm_nid_postfix)) == -2); + Require(_FDtest_nid_postfix(reinterpret_cast(&_FSnan_nid_postfix)) == 2); + Require(_LDenorm_nid_postfix._Long_double == std::numeric_limits::denorm_min()); + Require(_LInf_nid_postfix._Long_double == std::numeric_limits::infinity()); + Require(std::isnan(_LNan_nid_postfix._Long_double) && std::isnan(_LSnan_nid_postfix._Long_double)); +} + static void CheckMissingClassificationExports() { const float infinity = std::numeric_limits::infinity(); const float nan = std::numeric_limits::quiet_NaN(); @@ -840,6 +1126,11 @@ static void CheckScaling() { Require(std::signbit(significandf_nid_postfix(-0.0f)) && significandf_nid_postfix(-0.0f) == 0.0f); Require(significandf_nid_postfix(std::numeric_limits::infinity()) == std::numeric_limits::infinity()); Require(std::isnan(significandf_nid_postfix(std::numeric_limits::quiet_NaN()))); + Require(significand_nid_postfix(12.0) == 1.5 && significand_nid_postfix(-0.375) == -1.5); + Require(significand_nid_postfix(0x1p-1074) == 1.0); + Require(std::signbit(significand_nid_postfix(-0.0)) && significand_nid_postfix(-0.0) == 0.0); + Require(significand_nid_postfix(std::numeric_limits::infinity()) == std::numeric_limits::infinity()); + Require(std::isnan(significand_nid_postfix(std::numeric_limits::quiet_NaN()))); } static bool Near(long double actual, long double expected) { @@ -861,6 +1152,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); @@ -874,10 +1168,11 @@ static void CheckLongDoubleFunctions() { Require(hypot3_nid_postfix(0x1p1000, 0x1p1000, 0x1p1000) == std::hypot(0x1p1000, std::hypot(0x1p1000, 0x1p1000))); Require(j0f_nid_postfix(0.0f) == 1.0f && j1f_nid_postfix(0.0f) == 0.0f && jnf_nid_postfix(2, 0.0f) == 0.0f); - Require(std::fabs(j0f_nid_postfix(2.5f) - static_cast(std::cyl_bessel_j(0.0, 2.5))) < 1e-6f); - Require(std::fabs(jnf_nid_postfix(3, 4.0f) - static_cast(std::cyl_bessel_j(3.0, 4.0))) < 1e-6f); + Require(std::fabs(j0f_nid_postfix(2.5f) - static_cast(-0.048383776468198)) < 1e-6f); + Require(std::fabs(jnf_nid_postfix(3, 4.0f) - static_cast(0.430171473875622)) < 1e-6f); Require(y0f_nid_postfix(0.0f) == -std::numeric_limits::infinity() && y1f_nid_postfix(0.0f) == -std::numeric_limits::infinity()); - Require(std::fabs(ynf_nid_postfix(2, 3.0f) - static_cast(std::cyl_neumann(2.0, 3.0))) < 1e-6f); + Require(ynf_nid_postfix(2, 0.0f) == -std::numeric_limits::infinity()); + Require(std::fabs(ynf_nid_postfix(2, 3.0f) - static_cast(-0.160400393484924)) < 1e-6f); Require(std::isnan(y0f_nid_postfix(-1.0f))); } @@ -972,8 +1267,35 @@ static void CheckNoConversionIsPositiveZero() { Require(parsedWideLong == 0.L && !std::signbit(parsedWideLong) && wideEnd == wide); } +template +static void RequireCharacteristic(const unsigned char (&constant)[16], TValue expected) { + TValue value; + std::memcpy(&value, constant, sizeof(value)); + Require(value == expected); + for (std::size_t index = TBytes; index < 16; ++index) Require(constant[index] == 0); +} + +template +static void RequireCharacteristics(const DinkumwareFloatCharacteristics& table, const int (&integers)[6]) { + const int fields[] = {table.digits, table.mantissaDigits, table.maxExponent10, table.maxExponent, table.minExponent10, table.minExponent}; + for (int index = 0; index < 6; ++index) Require(fields[index] == integers[index]); + RequireCharacteristic(table.epsilon, std::numeric_limits::epsilon()); + RequireCharacteristic(table.max, std::numeric_limits::max()); + RequireCharacteristic(table.min, std::numeric_limits::min()); +} + +static void CheckDinkumwareFloatCharacteristics() { + static_assert(sizeof(DinkumwareFloatCharacteristics) == 80 && offsetof(DinkumwareFloatCharacteristics, epsilon) == 32); + RequireCharacteristics(_Flt_nid_postfix, {FLT_DIG, FLT_MANT_DIG, FLT_MAX_10_EXP, FLT_MAX_EXP, FLT_MIN_10_EXP, FLT_MIN_EXP}); + RequireCharacteristics(_Dbl_nid_postfix, {DBL_DIG, DBL_MANT_DIG, DBL_MAX_10_EXP, DBL_MAX_EXP, DBL_MIN_10_EXP, DBL_MIN_EXP}); + RequireCharacteristics(_Ldbl_nid_postfix, {18, 64, 4932, 16384, -4931, -16381}); + for (const auto* table : {&_Flt_nid_postfix, &_Dbl_nid_postfix, &_Ldbl_nid_postfix}) + for (std::size_t index = 24; index < 32; ++index) Require(reinterpret_cast(table)[index] == 0); +} + int main() { CheckFloatClassification(); + CheckDinkumwareConstants(); CheckNoConversionIsPositiveZero(); CheckMissingClassificationExports(); CheckFloatMathExports(); @@ -1143,6 +1465,13 @@ int main() { Require(nextafterf_nid_postfix(1.f, 1.f) == 1.f && !std::signbit(nextafterf_nid_postfix(-0.f, 0.f))); Require(nextafterf_nid_postfix(std::numeric_limits::max(), std::numeric_limits::infinity()) == std::numeric_limits::infinity()); Require(std::isnan(nextafterf_nid_postfix(std::numeric_limits::quiet_NaN(), 1.f)) && std::isnan(nextafterf_nid_postfix(1.f, std::numeric_limits::quiet_NaN()))); + Require(nextafter_nid_postfix(1.0, 2.0) == 0x1.0000000000001p0 && nextafter_nid_postfix(1.0, 0.0) == 0x1.fffffffffffffp-1); + Require(nextafter_nid_postfix(0.0, 1.0) == std::numeric_limits::denorm_min()); + Require(nextafter_nid_postfix(0.0, -1.0) == -std::numeric_limits::denorm_min()); + Require(nextafter_nid_postfix(std::numeric_limits::denorm_min(), -1.0) == 0.0); + Require(nextafter_nid_postfix(1.0, 1.0) == 1.0 && !std::signbit(nextafter_nid_postfix(-0.0, 0.0))); + Require(nextafter_nid_postfix(std::numeric_limits::max(), std::numeric_limits::infinity()) == std::numeric_limits::infinity()); + Require(std::isnan(nextafter_nid_postfix(std::numeric_limits::quiet_NaN(), 1.0)) && std::isnan(nextafter_nid_postfix(1.0, std::numeric_limits::quiet_NaN()))); int exponent = 0; Require(frexp_nid_postfix(12., &exponent) == 0.75 && exponent == 4); Require(frexpf_nid_postfix(-12.f, &exponent) == -0.75f && exponent == 4); @@ -1248,12 +1577,15 @@ int main() { Require(std::isinf(__powidf2_nid_postfix(-0.0, -3)) && std::signbit(__powidf2_nid_postfix(-0.0, -3))); Require(__powidf2_nid_postfix(2.0, -127) == 0x1p-127); CheckHyperbolic(); + CheckScaledHyperbolic(); CheckSinhf(); CheckAtanhf(); CheckHypotSpecialValue(); + CheckFloatUnderflowErrno(); CheckFmodErrno(); CheckDoubleClassification(); CheckIntegerAbsAndDivision(); CheckScaling(); CheckLongDoubleFunctions(); + CheckDinkumwareFloatCharacteristics(); } diff --git a/core/libs/tests/GuestMathExact.cpp b/core/libs/tests/GuestMathExact.cpp index b3afbca1ac..e601e88973 100644 --- a/core/libs/tests/GuestMathExact.cpp +++ b/core/libs/tests/GuestMathExact.cpp @@ -1,5 +1,6 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include +#include #include #include #include @@ -22,6 +23,10 @@ long double APS5_VABI remainderl_nid_postfix(long double, long double); double APS5_VABI drem_nid_postfix(double, double); float APS5_VABI dremf_nid_postfix(float, float); std::int64_t APS5_VABI lrint_nid_postfix(double); +double APS5_VABI rint_nid_postfix(double); +float APS5_VABI rintf_nid_postfix(float); +long double APS5_VABI rintl_nid_postfix(long double); +long long APS5_VABI llrint_nid_postfix(double); int APS5_VABI finite_nid_postfix(double); int APS5_VABI __fpclassifyl_nid_postfix(long double); int APS5_VABI __isnanl_nid_postfix(long double); @@ -96,6 +101,25 @@ static void CheckConsoleQuirks() { Require(finite_nid_postfix(1.0) == 1 && finite_nid_postfix(INFINITY) == 0); } +static void CheckRoundingModes() { + struct Case { + int mode; + double negative; + double positive; + }; + const Case cases[] = {{FE_TONEAREST, -2.0, 2.0}, {FE_DOWNWARD, -2.0, 1.0}, {FE_UPWARD, -1.0, 2.0}, {FE_TOWARDZERO, -1.0, 1.0}}; + const int saved = std::fegetround(); + for (const auto& test : cases) { + Require(std::fesetround(test.mode) == 0); + Require(rint_nid_postfix(-1.5) == test.negative && rint_nid_postfix(1.5) == test.positive); + Require(rintf_nid_postfix(-1.5f) == static_cast(test.negative) && rintf_nid_postfix(1.5f) == static_cast(test.positive)); + Require(rintl_nid_postfix(-1.5L) == test.negative && rintl_nid_postfix(1.5L) == test.positive); + Require(lrint_nid_postfix(-1.5) == static_cast(test.negative) && llrint_nid_postfix(1.5) == static_cast(test.positive)); + Require(std::fegetround() == test.mode); + } + Require(std::fesetround(saved) == 0); +} + static void CheckClassification() { const long double pseudoDenormal = Extended(0x0000, 0x8000000000000000ull); Require(_LDtest_nid_postfix(&pseudoDenormal) == -1 && __fpclassifyl_nid_postfix(pseudoDenormal) == -2); @@ -112,6 +136,7 @@ int main() { CheckSignalingNanPassThrough(); CheckErrno(); CheckConsoleQuirks(); + CheckRoundingModes(); CheckClassification(); return 0; } diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index e9a8d30f0e..a52c5a5e2d 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -208,12 +208,83 @@ static void CheckAudioCoprocessorProtection() { static void CheckMprotectIgnoresUndefinedBits() { constexpr std::size_t length = 0x4000; + const auto check = [&](void* mapping, int protection) { + Require(sceKernelMprotect(mapping, length, protection) == 0); + VirtualQueryInfo info{}; + Require(sceKernelVirtualQuery(mapping, 0, &info, sizeof(info)) == 0); + int expected = protection & 0x3f7; + if ((expected & 0x2) != 0) expected |= 0x1; + if ((expected & 0x80) != 0) expected |= 0x40; + if ((expected & 0x200) != 0) expected |= 0x100; + if (info.protection != expected) + std::fprintf(stderr, "Memory protection 0x%x reported 0x%x, expected 0x%x\n", static_cast(protection), static_cast(info.protection), static_cast(expected)); + Require(info.protection == expected); + auto* bytes = static_cast(mapping); + if ((protection & 0x222) != 0) { + bytes[0] = 5; + bytes[length - 1] = 7; + Require(bytes[0] == 5 && bytes[length - 1] == 7); + } else if ((protection & 0x117) != 0) { + Require(bytes[0] == 5 && bytes[length - 1] == 7); + } + }; void* mapping = nullptr; Require(sceKernelMapFlexibleMemory(&mapping, length, 3, 0) == 0); - Require(sceKernelMprotect(mapping, length, static_cast(0xfffffff2u)) == 0); - static_cast(mapping)[length - 1] = 5; - Require(static_cast(mapping)[length - 1] == 5); + for (int byte = 0x80; byte <= 0xff; ++byte) check(mapping, byte - 0x100); + for (const int protection : {0, 1, 2, 3, 4, 0x10, 0x20, 0x40, 0x80, 0x100, 0x200, 0x3f7}) { + for (const auto undefined : {0u, 8u, 0x400u, 0x80000000u, 0xfffffc08u}) + check(mapping, static_cast(static_cast(protection) | undefined)); + } Require(sceKernelMunmap(mapping, length) == 0); + std::int64_t physical = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, length, 0, 0, &physical) == 0); + mapping = nullptr; + Require(sceKernelMapDirectMemory(&mapping, length, 0xf2, 0, physical, 0) == 0); + check(mapping, -14); + check(mapping, 0x400); + check(mapping, 0x100); + check(mapping, 0x200); + check(mapping, 0x3f7); + Require(sceKernelMunmap(mapping, length) == 0); + Require(sceKernelReleaseDirectMemory(physical, length) == 0); +} + +static void CheckStrictMemoryProtectionBits() { + constexpr std::size_t length = 0x4000; + std::int64_t physical = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, length, 0, 0, &physical) == 0); + void* mapping = nullptr; + Require(sceKernelMapDirectMemory(&mapping, length, 3, 0, physical, 0) == 0); + auto* bytes = static_cast(mapping); + bytes[0] = 5; + bytes[length - 1] = 7; + const auto rejected = [&](auto operation) { + bool threw = false; + try { operation(); } catch (const std::invalid_argument&) { threw = true; } + Require(threw); + }; + std::size_t available = 0; + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0); + for (const int protection : {-256, -129, -128, -14, -1, 8, 0x400, std::numeric_limits::min()}) { + void* invalid = nullptr; + rejected([&] { sceKernelMapFlexibleMemory(&invalid, length, protection, 0); }); + Require(invalid == nullptr); + std::size_t after = 0; + Require(sceKernelAvailableFlexibleMemorySize(&after) == 0 && after == available); + rejected([&] { sceKernelMapDirectMemory(&invalid, length, protection, 0, physical, 0); }); + Require(invalid == nullptr); + rejected([&] { sceKernelMtypeprotect(mapping, length, 2, protection); }); + VirtualQueryInfo info{}; + Require(sceKernelVirtualQuery(mapping, 0, &info, sizeof(info)) == 0); + Require(info.protection == 3 && info.memory_type == 0); + int type = -1; + std::int64_t start = -1; + std::int64_t end = -1; + Require(sceKernelGetDirectMemoryType(physical, &type, &start, &end) == 0 && type == 0); + Require(bytes[0] == 5 && bytes[length - 1] == 7); + } + Require(sceKernelMunmap(mapping, length) == 0); + Require(sceKernelReleaseDirectMemory(physical, length) == 0); } static void CheckInternalNamedFlexibleMapping() { @@ -484,6 +555,119 @@ static void CheckSparseDirectMemoryBacking() { Require(sceKernelReleaseDirectMemory(physical, size) == 0); } +static void CheckSavedDirectMemorySurvivesCachePressure() { +#ifdef _WIN32 + constexpr std::size_t bytes = 0x200000; + std::array retained{}; + for (std::size_t index = 0; index < retained.size(); ++index) { + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, bytes, 0, 0, &retained[index]) == 0); + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, bytes, 3, 0, retained[index], bytes) == 0); + std::memset(mapped, static_cast(index + 1), bytes); + Require(sceKernelMunmap(mapped, bytes) == 0); + } + const auto temporary = [] { + constexpr std::size_t small = 0x10000; + for (unsigned char round = 1; round <= 24; ++round) { + std::int64_t physical = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, small, 0, 0, &physical) == 0); + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, small, 3, 0, physical, 0) == 0); + auto* data = static_cast(mapped); + Require(std::all_of(data, data + small, [](unsigned char value) { return value == 0; })); + data[7] = round; + data[small - 1] = static_cast(round + 0x40); + Require(sceKernelMunmap(mapped, small) == 0); + mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, small, 3, 0, physical, 0) == 0); + data = static_cast(mapped); + for (std::size_t at = 0; at < small; ++at) Require(data[at] == (at == 7 ? round : at == small - 1 ? static_cast(round + 0x40) : 0)); + Require(sceKernelMunmap(mapped, small) == 0); + Require(sceKernelReleaseDirectMemory(physical, small) == 0); + } + }; + temporary(); + for (std::size_t index = 0; index < retained.size(); ++index) { + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, bytes, 3, 0, retained[index], bytes) == 0); + const auto* data = static_cast(mapped); + Require(std::all_of(data, data + bytes, [&](unsigned char value) { return value == index + 1; })); + Require(sceKernelMunmap(mapped, bytes) == 0); + Require(sceKernelReleaseDirectMemory(retained[index], bytes) == 0); + } + temporary(); +#endif +} + +static void CheckReusedDirectMemorySections() { +#ifdef _WIN32 + constexpr std::size_t page = 0x4000; + constexpr std::size_t bytes = page * 4; + const bool pooled = std::getenv("APS5_NO_DIRECT_MEMORY_SECTION_POOL") == nullptr; + const auto allocate = [] { + std::int64_t physical = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, bytes, 0, 0, &physical) == 0); + return physical; + }; + const auto map = [](std::int64_t physical) { + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, bytes, 3, 0, physical, 0) == 0); + return static_cast(mapped); + }; + const auto blank = [](const unsigned char* data) { return std::all_of(data, data + bytes, [](unsigned char value) { return value == 0; }); }; + const auto handles = [] { + DWORD count = 0; + Require(GetProcessHandleCount(GetCurrentProcess(), &count) != 0); + return count; + }; + auto physical = allocate(); + auto* data = map(physical); + std::memset(data, 0x5a, bytes); + Require(sceKernelMunmap(data, bytes) == 0); + auto before = handles(); + Require(sceKernelReleaseDirectMemory(physical, bytes) == 0); + const auto released = handles(); + Require(released == (pooled ? before : before - 1)); + physical = allocate(); + Require(handles() == (pooled ? released : released + 1)); + data = map(physical); + Require(blank(data)); + data[5] = 17; + data[bytes - 1] = 29; + void* second = nullptr; + Require(sceKernelMapDirectMemory(&second, bytes, 3, 0, physical, 0) == 0); + auto* alias = static_cast(second); + Require(alias != data && alias[5] == 17 && alias[6] == 0 && alias[bytes - 1] == 29); + alias[page + 3] = 41; + Require(data[page + 3] == 41); + data[page * 2] = 53; + Require(alias[page * 2] == 53); + Require(sceKernelMunmap(alias, bytes) == 0); + Require(data[5] == 17 && data[page + 3] == 41 && data[page * 2] == 53); + Require(sceKernelMunmap(data, bytes) == 0); + data = map(physical); + Require(data[5] == 17 && data[page + 3] == 41 && data[page * 2] == 53 && data[bytes - 1] == 29); + Require(sceKernelMunmap(data, bytes) == 0); + before = handles(); + Require(sceKernelReleaseDirectMemory(physical, bytes) == 0); + Require(handles() == before - 1); + for (unsigned char round = 1; round <= 8; ++round) { + physical = allocate(); + data = map(physical); + Require(blank(data)); + std::memset(data, round, bytes); + if (round % 2 == 0) { + second = nullptr; + Require(sceKernelMapDirectMemory(&second, page, 3, 0, physical + static_cast(page), 0) == 0); + Require(static_cast(second)[page - 1] == round); + Require(sceKernelMunmap(second, page) == 0); + } + Require(sceKernelMunmap(data, bytes) == 0); + Require(sceKernelReleaseDirectMemory(physical, bytes) == 0); + } +#endif +} + static void CheckReleaseDirectMemoryClearsMappings() { constexpr std::size_t page = 0x4000; std::int64_t phys = 0; @@ -1570,6 +1754,264 @@ static void CheckSingleDirectMappingTracksWritesWithoutFaults() { #endif } +static void CheckLargeResidentWritesStopFaulting() { +#ifdef _WIN32 + constexpr std::size_t bytes = 0x200000; + std::int64_t physical = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, bytes, 0, 0, &physical) == 0); + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, bytes, 3, 0, physical, bytes) == 0); + const auto collect = [&] { + std::array pages{}; + std::size_t count = pages.size(); + Require(GuestArena::GuestArenaCollectWrites_nid_postfix(reinterpret_cast(mapped), bytes, pages.data(), &count, true)); + return count; + }; + Require(collect() == bytes / 4096); + Require(collect() == 0 && collect() == 0); + MEMORY_BASIC_INFORMATION memory{}; + Require(VirtualQuery(mapped, &memory, sizeof(memory)) == sizeof(memory) && memory.Protect == PAGE_READONLY); + auto* data = static_cast(mapped); + const auto path = std::filesystem::path("anyps5-resident-read-" + + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".bin"); + const std::vector input(65536, 89); + { + std::ofstream stream(path, std::ios::binary); + stream.write(input.data(), static_cast(input.size())); + } + const int fd = sceKernelOpen(path.string().c_str(), SCE_KERNEL_O_RDONLY, 0); + Require(fd >= 0); + Require(sceKernelPread(fd, mapped, input.size(), 0) == static_cast(input.size())); + Require(data[0] == 89 && data[input.size() - 1] == 89); + Require(collect() == input.size() / 4096 && collect() == 0); + Require(sceKernelRead(fd, data + input.size(), input.size()) == static_cast(input.size())); + Require(data[input.size()] == 89 && data[input.size() * 2 - 1] == 89); + Require(collect() == input.size() / 4096 && collect() == 0); + Require(sceKernelClose(fd) == 0); + std::filesystem::remove(path); + { + GuestArena::HostImportAccess access(mapped, bytes); + Require(VirtualQuery(mapped, &memory, sizeof(memory)) == sizeof(memory) && memory.Protect == PAGE_READWRITE); + Require(collect() == 0 && collect() == 0); + Require(VirtualQuery(mapped, &memory, sizeof(memory)) == sizeof(memory) && memory.Protect == PAGE_READWRITE); + data[17] = 3; + Require(collect() == 1 && collect() == 0); + } + for (unsigned int round = 0; round < 16; ++round) Require(collect() == 0); + Require(VirtualQuery(mapped, &memory, sizeof(memory)) == sizeof(memory) && memory.Protect == PAGE_READONLY); + const auto before = g_accessViolations; + const auto handler = AddVectoredExceptionHandler(1, CountAccessViolations); + Require(handler != nullptr); + for (unsigned int round = 0; round < 16; ++round) { + for (std::size_t at = 0; at < bytes; at += 4096) static_cast(mapped)[at] = static_cast(round); + Require(collect() == bytes / 4096 && collect() == 0); + } + Require(RemoveVectoredExceptionHandler(handler)); + Require(g_accessViolations == before + 1); + Require(VirtualQuery(mapped, &memory, sizeof(memory)) == sizeof(memory) && memory.Protect == PAGE_READWRITE); + Require(mprotect_nid_postfix(mapped, bytes, 1) == 0); + Require(collect() == 0 && collect() == 0); + Require(mprotect_nid_postfix(mapped, bytes, 3) == 0); + data[0x10000] = 97; + Require(collect() == 1 && collect() == 0); + Require(sceKernelMunmap(mapped, bytes) == 0); + Require(sceKernelReleaseDirectMemory(physical, bytes) == 0); +#endif +} + +static void CheckImportAccessDoesNotInventSharedWrites() { +#ifdef _WIN32 + constexpr std::size_t bytes = 0x100000; + std::int64_t physical = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, bytes, 0, 0, &physical) == 0); + void* first = nullptr; + void* second = nullptr; + Require(sceKernelMapDirectMemory(&first, bytes, 3, 0, physical, 0) == 0); + Require(sceKernelMapDirectMemory(&second, bytes, 3, 0, physical, 0) == 0); + const auto collect = [&](void* pointer) { + std::array pages{}; + std::size_t count = pages.size(); + Require(GuestArena::GuestArenaCollectWrites_nid_postfix(reinterpret_cast(pointer), bytes, pages.data(), &count, true)); + return count; + }; + collect(first); + collect(second); + for (unsigned int round = 0; round < 16; ++round) { + { + GuestArena::HostImportAccess access(first, bytes); + Require(collect(first) == 0 && collect(second) == 0); + } + Require(collect(first) == 0 && collect(second) == 0); + } + { + GuestArena::HostImportAccess access(first, bytes); + static_cast(first)[4097] = 73; + Require(collect(first) == 1 && collect(second) == 1); + } + Require(static_cast(second)[4097] == 73); + Require(collect(first) == 0 && collect(second) == 0); + Require(sceKernelMunmap(first, bytes) == 0); + Require(sceKernelMunmap(second, bytes) == 0); + Require(sceKernelReleaseDirectMemory(physical, bytes) == 0); +#endif +} + +static void CheckResidentCollectPreservesPartialWrites() { +#ifdef _WIN32 + constexpr std::size_t bytes = 0x210000; + constexpr std::size_t small = 0x1000; + std::int64_t physical = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, bytes, 0, 0, &physical) == 0); + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, bytes, 3, 0, physical, 0x200000) == 0); + const auto address = reinterpret_cast(mapped); + const auto collect = [&](std::size_t offset, std::size_t length, std::size_t capacity, bool clear, bool armed) { + unsigned char sentinel = 0; + std::vector pages(capacity + 2, &sentinel); + std::size_t count = capacity; + const auto function = armed ? &GuestArena::GuestArenaCollectArmedWrites_nid_postfix : &GuestArena::GuestArenaCollectWrites_nid_postfix; + Require(function(address + offset, length, pages.data() + 1, &count, clear)); + Require(count <= capacity && pages.front() == &sentinel && pages.back() == &sentinel); + std::vector found; + for (std::size_t at = 0; at < count; ++at) found.push_back(reinterpret_cast(pages[at + 1]) - address); + return found; + }; + Require(collect(0, bytes, 0, true, false).empty()); + Require(collect(0, bytes, bytes / small, true, false).size() == bytes / small); + Require(collect(0, bytes, bytes / small, true, true).empty()); + auto* data = static_cast(mapped); + data[3] = 1; + data[small + 8] = 2; + data[0x200000 + 9] = 3; + Require(collect(0, bytes, 0, true, false).empty()); + Require(collect(1, small - 1, 1, false, true) == std::vector{1}); + Require(collect(0, small, 1, false, false) == std::vector{0}); + Require(collect(0, small, 1, true, true) == std::vector{0}); + Require(collect(0, small, 1, true, false).empty()); + Require(collect(small, bytes - small, 1, true, false) == std::vector{small}); + Require(collect(small * 2, bytes - small * 2, bytes / small, true, true) == std::vector{0x200000}); + Require(collect(0, bytes, bytes / small, true, false).empty()); + Require(sceKernelMunmap(mapped, bytes) == 0); + Require(sceKernelReleaseDirectMemory(physical, bytes) == 0); +#endif +} + +static void CheckResidentCollectionDuringHostWrites() { +#ifdef _WIN32 + constexpr std::size_t bytes = 0x100000; + constexpr std::size_t small = 0x1000; + std::int64_t physical = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, bytes, 0, 0, &physical) == 0); + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, bytes, 3, 0, physical, 0x200000) == 0); + auto* data = static_cast(mapped); + const auto address = reinterpret_cast(mapped); + const auto collect = [&] { + std::array pages{}; + std::size_t count = pages.size(); + Require(GuestArena::GuestArenaCollectWrites_nid_postfix(address, bytes, pages.data(), &count, true)); + return std::vector(pages.begin(), pages.begin() + count); + }; + collect(); + Require(collect().empty()); + Require(GuestArena::GuestArenaBeginHostWrite_nid_postfix(data + small, small)); + data[small + 7] = 1; + Require(collect() == std::vector{data + small}); + data[small + 8] = 2; + GuestArena::GuestArenaEndHostWrite_nid_postfix(data + small, small); + Require(collect() == std::vector{data + small}); + Require(collect().empty()); + GuestArena::GuestArenaPinWritable_nid_postfix(data + small * 2, small); + collect(); + static_cast(mapped)[small * 2 + 9] = 3; + Require(collect() == std::vector{data + small * 2}); + GuestArena::GuestArenaUnpinWritable_nid_postfix(data + small * 2, small); + collect(); + std::atomic written{0}; + std::atomic consumed{0}; + std::thread writer([&] { + for (unsigned int round = 1; round <= 128; ++round) { + while (consumed.load(std::memory_order_acquire) != round - 1) std::this_thread::yield(); + static_cast(mapped)[small * 3 + 7] = static_cast(round); + written.store(round, std::memory_order_release); + } + }); + for (unsigned int round = 1; round <= 128; ++round) { + const auto racingPages = collect(); + while (written.load(std::memory_order_acquire) != round) std::this_thread::yield(); + const auto pages = collect(); + Require(std::find(pages.begin(), pages.end(), data + small * 3) != pages.end() || std::find(racingPages.begin(), racingPages.end(), data + small * 3) != racingPages.end()); + Require(data[small * 3 + 7] == static_cast(round)); + Require(collect().empty()); + consumed.store(round, std::memory_order_release); + } + writer.join(); + void* alias = nullptr; + Require(sceKernelMapDirectMemory(&alias, 0x4000, 3, 0, physical, 0) == 0); + collect(); + static_cast(alias)[small + 11] = 4; + Require(data[small + 11] == 4); + const auto pages = collect(); + Require(std::find(pages.begin(), pages.end(), data + small) != pages.end()); + Require(sceKernelMunmap(alias, 0x4000) == 0); + Require(sceKernelMunmap(mapped, bytes) == 0); + Require(sceKernelReleaseDirectMemory(physical, bytes) == 0); +#endif +} + +static void CheckReleaseKeepsPhysicalUntilViewsUnmapped(decltype(&sceKernelReleaseDirectMemory) release) { + constexpr std::size_t page = 0x4000; + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 4, 0, 0, &phys) == 0); + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, page * 4, 3, 0, phys, 0) == 0); + auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + Require(std::count_if(lease.begin(), lease.end(), [&](const auto& range) { return range->address == reinterpret_cast(mapped); }) == 1); + std::atomic started{false}; + std::atomic finished{false}; + std::thread releaser([&] { + started = true; + Require(release(phys, page * 4) == 0); + finished = true; + }); + while (!started) std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + Require(!finished); + std::int64_t again = 0; + Require(sceKernelAllocateDirectMemory(phys, phys + static_cast(page * 4), page * 4, 0, 0, &again) != 0); + lease.clear(); + releaser.join(); + Require(finished); + Require(sceKernelAllocateDirectMemory(phys, phys + static_cast(page * 4), page * 4, 0, 0, &again) == 0 && again == phys); + Require(sceKernelReleaseDirectMemory(again, page * 4) == 0); +} + +static void CheckConcurrentCheckedReleasesUnmapViewsOnce() { + constexpr std::size_t page = 0x4000; + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 4, 0, 0, &phys) == 0); + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, page * 4, 3, 0, phys, 0) == 0); + auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + std::atomic started{0}; + int results[2] = {-1, -1}; + std::thread releasers[2]; + for (int index = 0; index < 2; index++) { + releasers[index] = std::thread([&, index] { + started++; + results[index] = sceKernelCheckedReleaseDirectMemory(phys, page * 4); + }); + } + while (started != 2) std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + lease.clear(); + for (auto& releaser : releasers) releaser.join(); + Require((results[0] == 0 && results[1] == SCE_KERNEL_ERROR_ENOENT) || (results[0] == SCE_KERNEL_ERROR_ENOENT && results[1] == 0)); + std::int64_t again = 0; + Require(sceKernelAllocateDirectMemory(phys, phys + static_cast(page * 4), page * 4, 0, 0, &again) == 0 && again == phys); + Require(sceKernelReleaseDirectMemory(again, page * 4) == 0); +} + static void CheckSecondDirectMappingWaitsForLeases() { #ifdef _WIN32 constexpr std::size_t page = 0x4000; @@ -2778,8 +3220,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); @@ -2843,8 +3288,11 @@ int main(int argc, char** argv) { CheckCheckedReleaseDirectMemory(); CheckAudioCoprocessorProtection(); CheckMprotectIgnoresUndefinedBits(); + CheckStrictMemoryProtectionBits(); CheckDirectMemoryFollowsPhysicalPages(); CheckSparseDirectMemoryBacking(); + CheckSavedDirectMemorySurvivesCachePressure(); + CheckReusedDirectMemorySections(); CheckReleaseDirectMemoryClearsMappings(); CheckReleaseDirectMemoryRejectsInvalidRanges(); CheckFixedMappingReplacesPartialOverlap(); @@ -2891,7 +3339,14 @@ int main(int argc, char** argv) { CheckPinnedSharedPages(); CheckFailedCollectKeepsWrites(); CheckSingleDirectMappingTracksWritesWithoutFaults(); + CheckLargeResidentWritesStopFaulting(); + CheckImportAccessDoesNotInventSharedWrites(); + CheckResidentCollectPreservesPartialWrites(); + CheckResidentCollectionDuringHostWrites(); CheckSecondDirectMappingWaitsForLeases(); + CheckReleaseKeepsPhysicalUntilViewsUnmapped(sceKernelReleaseDirectMemory); + CheckReleaseKeepsPhysicalUntilViewsUnmapped(sceKernelCheckedReleaseDirectMemory); + CheckConcurrentCheckedReleasesUnmapViewsOnce(); CheckBreakingAResidentRunKeepsConcurrentAccesses(); CheckRemappedDirectMemoryIsChargedOnce(); #if defined(__linux__) diff --git a/core/libs/tests/GuestMktime.cpp b/core/libs/tests/GuestMktime.cpp index beaf273370..97d72aeaa9 100644 --- a/core/libs/tests/GuestMktime.cpp +++ b/core/libs/tests/GuestMktime.cpp @@ -13,6 +13,7 @@ std::int64_t APS5_VABI libc_mktime_nid_postfix(GuestTm*); std::int64_t APS5_VABI mktime_nid_postfix(GuestTm*); GuestTm* APS5_VABI localtime_s_nid_postfix(const std::int64_t*, GuestTm*); void APS5_VABI tzset_nid_postfix(); +int APS5_VABI setenv_nid_postfix(const char*, const char*, int); } using Converter = std::int64_t (APS5_VABI *)(GuestTm*); @@ -64,11 +65,7 @@ static void CheckFailure(Converter convert) { } static void CheckZone(const char* zone) { -#ifdef _WIN32 - Require(_putenv_s("TZ", zone) == 0, "Could not set the time zone"); -#else - Require(setenv("TZ", zone, 1) == 0, "Could not set the time zone"); -#endif + Require(setenv_nid_postfix("TZ", zone, 1) == 0, "Could not set the time zone"); tzset_nid_postfix(); constexpr std::array converters{libc_mktime_nid_postfix, mktime_nid_postfix}; constexpr std::array timers{-86401, -2, -1, 0, 1, 59, 60, 86400}; diff --git a/core/libs/tests/GuestMove.cpp b/core/libs/tests/GuestMove.cpp new file mode 100644 index 0000000000..b114030e35 --- /dev/null +++ b/core/libs/tests/GuestMove.cpp @@ -0,0 +1,149 @@ +#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 int kInvalidHandle = static_cast(0x80EE0004); +constexpr int kAlreadyOpened = static_cast(0x80EE0007); +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 RequireUnknownHandle(std::int32_t handle) { + Buffer buffer; + std::int32_t count = -7; + Require(sceMoveClose(handle) == kInvalidHandle); + Require(sceMoveGetDeviceInfo(handle, buffer.bytes) == kInvalidHandle); + Require(sceMoveGetExtensionPortInfo(handle, buffer.bytes) == kInvalidHandle); + Require(sceMoveReadStateLatest(handle, buffer.bytes) == kInvalidHandle); + Require(sceMoveReadStateRecent(handle, 0, buffer.bytes, &count) == kInvalidHandle); + Require(sceMoveSetVibration(handle, 0xFF) == kInvalidHandle); + Require(sceMoveSetLightSphere(handle, 0xFF, 0, 0) == kInvalidHandle); + Require(sceMoveResetLightSphere(handle) == kInvalidHandle); + Require(sceMoveGetDeviceInfo(handle, nullptr) == kInvalidHandle); + Require(sceMoveReadStateRecent(handle, -1, nullptr, nullptr) == kInvalidHandle); + Require(buffer.Untouched()); + Require(count == -7); +} + +} + +int main() { + RequireNotInit(kFirstHandle); + + Require(sceMoveInit() == kOk); + Require(sceMoveInit() == kAlreadyInit); + + RequireUnknownHandle(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(sceMoveOpen(kUserId, 0, 0) == kAlreadyOpened); + Require(sceMoveOpen(kUserId, 0, 1) == kAlreadyOpened); + 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); + RequireUnknownHandle(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); + RequireUnknownHandle(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/GuestMsgDialogNative.cpp b/core/libs/tests/GuestMsgDialogNative.cpp index b25419c969..8842c84299 100644 --- a/core/libs/tests/GuestMsgDialogNative.cpp +++ b/core/libs/tests/GuestMsgDialogNative.cpp @@ -4,6 +4,7 @@ #include extern "C" { +extern int SceMsgDialogNativeModuleLoaded_nid_no_patch; int APS5_VABI sceMsgDialogInitialize(void); int APS5_VABI sceMsgDialogOpen(const void* param); int APS5_VABI sceMsgDialogGetStatus(void); @@ -32,6 +33,7 @@ constexpr int kButtonIdOk = 1; } int main() { + Require(SceMsgDialogNativeModuleLoaded_nid_no_patch == 1); std::uint8_t param[0x60]{}; MsgDialogResult result{}; diff --git a/core/libs/tests/GuestNetCtl.cpp b/core/libs/tests/GuestNetCtl.cpp index 5ee4c2a871..ea68f8eb02 100644 --- a/core/libs/tests/GuestNetCtl.cpp +++ b/core/libs/tests/GuestNetCtl.cpp @@ -37,7 +37,9 @@ int APS5_VABI sceNetCtlGetInfo(int, NetCtlInfo*); int APS5_VABI sceNetCtlGetNatInfo(NetCtlNatInfo*); int APS5_VABI sceNetCtlGetState(int*); int APS5_VABI sceNetCtlGetStateV6(int*); +int APS5_VABI sceNetCtlGetInfoV6(int, void*); int APS5_VABI sceNetCtlGetResult(int, int*); +int APS5_VABI sceNetCtlGetResultV6(int, int*); #ifdef __APPLE__ std::size_t NetCtlDefaultRoutes_nid_no_patch(const char*, std::size_t, NetCtlDefaultRoute*, std::size_t); #endif @@ -270,11 +272,28 @@ int main() { Require(sceNetCtlGetResult(0, nullptr) == kErrInvalidAddr); int errorCode = -1; Require(sceNetCtlGetResult(0, &errorCode) == 0 && errorCode == 0); + Require(sceNetCtlGetResultV6(kEventIpObtained, nullptr) == kErrInvalidAddr); + for (const int eventType : {kEventDisconnected, 2, kEventIpObtained}) { + int errorCodeV6 = -1; + Require(sceNetCtlGetResultV6(eventType, &errorCodeV6) == 0 && errorCodeV6 == 0); + } + for (const int eventType : {0, 4, -1}) { + int errorCodeV6 = -1; + bool threw = false; + try { sceNetCtlGetResultV6(eventType, &errorCodeV6); } catch (const std::runtime_error&) { threw = true; } + Require(threw && errorCodeV6 == -1); + } NetCtlInfo info{}; Require(sceNetCtlGetInfo(0, &info) == kErrInvalidCode); Require(sceNetCtlGetInfo(22, &info) == kErrInvalidCode); Require(sceNetCtlGetInfo(14, nullptr) == kErrInvalidAddr); + std::uint8_t infoV6[256] = {}; + Require(sceNetCtlGetInfoV6(0, infoV6) == kErrInvalidCode); + Require(sceNetCtlGetInfoV6(6, infoV6) == kErrInvalidCode); + Require(sceNetCtlGetInfoV6(1, nullptr) == kErrInvalidAddr); + if (stateV6 != kStateIpObtained) Require(sceNetCtlGetInfoV6(1, infoV6) == kErrNotConnected); + const int result = sceNetCtlGetInfo(14, &info); if (result == kErrNotConnected) { std::puts("skipped, the host has no IPv4 address"); 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/GuestNpAuthAuthorizedAppDialog.cpp b/core/libs/tests/GuestNpAuthAuthorizedAppDialog.cpp index e8a0a3afc8..7eecd54c43 100644 --- a/core/libs/tests/GuestNpAuthAuthorizedAppDialog.cpp +++ b/core/libs/tests/GuestNpAuthAuthorizedAppDialog.cpp @@ -58,5 +58,14 @@ int main() { Require(sceNpAuthAuthorizedAppDialogTerminate() == 0); Require(sceNpAuthAuthorizedAppDialogTerminate() == kErrNotInitialized); Require(sceNpAuthAuthorizedAppDialogGetStatus() == kStatusNone); + + Require(sceNpAuthAuthorizedAppDialogInitialize() == 0); + Require(sceNpAuthAuthorizedAppDialogGetStatus() == kStatusInitialized); + Require(sceNpAuthAuthorizedAppDialogClose() == 0); + Require(sceNpAuthAuthorizedAppDialogGetStatus() == kStatusFinished); + result[0] = -1; + Require(sceNpAuthAuthorizedAppDialogGetResult(result) == 0); + Require(result[0] == kResultUserCanceled); + Require(sceNpAuthAuthorizedAppDialogTerminate() == 0); return 0; } diff --git a/core/libs/tests/GuestNpCppWebApiIntrusivePtr.cpp b/core/libs/tests/GuestNpCppWebApiIntrusivePtr.cpp new file mode 100644 index 0000000000..99f3f83152 --- /dev/null +++ b/core/libs/tests/GuestNpCppWebApiIntrusivePtr.cpp @@ -0,0 +1,321 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include + +using Deleter = void (APS5_VABI*)(void*); + +struct IntrusivePtr { + void* object; + Deleter deleter; + void* libContext; +}; + +using Construct = void (APS5_VABI*)(IntrusivePtr*); +using Copy = void (APS5_VABI*)(IntrusivePtr*, const IntrusivePtr*); +using Destroy = void (APS5_VABI*)(IntrusivePtr*); +using Access = void* (APS5_VABI*)(const IntrusivePtr*); + +extern "C" { +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEEC1Ev(IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEEC1Ev(IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEC1Ev(IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEC1Ev(IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEEC1Ev(IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V14UserEEC1Ev(IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V19RecordApi26RecordScoreResponseHeadersEEC1Ev(IntrusivePtr*); + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEEC1ERS7_(IntrusivePtr*, const IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEC1ERS7_(IntrusivePtr*, const IntrusivePtr*); + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEED1Ev(IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEED1Ev(IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEED1Ev(IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEED1Ev(IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEED1Ev(IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V14UserEED1Ev(IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V19RecordApi26RecordScoreResponseHeadersEED1Ev(IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_12Leaderboards2V15EntryEEEEEED1Ev(IntrusivePtr*); + +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEEptEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEEptEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEptEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEptEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEEptEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V14UserEEptEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V15EntryEEptEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V19RecordApi26RecordScoreResponseHeadersEEptEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6BinaryEEptEv(const IntrusivePtr*); +void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_12Leaderboards2V15EntryEEEEEEdeEv(const IntrusivePtr*); + +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V126RequestPlayerSessionPlayerEED1Ev(IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V126RequestPlayerSessionPlayerEEEEC1EPNS2_10LibContextE(IntrusivePtr*, void*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V126RequestPlayerSessionPlayerEEEED1Ev(IntrusivePtr*); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V126RequestPlayerSessionPlayerEEEE8pushBackERKS8_(IntrusivePtr*, const IntrusivePtr*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS2_6StringEEEEC1EPS6_PFvS8_EPNS2_10LibContextE(IntrusivePtr*, void*, Deleter, void*); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS2_6StringEEEED1Ev(IntrusivePtr*); +} + +static void Require(bool value) { if (!value) std::abort(); } + +static constexpr std::size_t ObjectSize = 0x40; +static constexpr std::size_t VectorReferenceCountOffset = 0x30; + +struct DestroyCase { + Destroy destroy; + std::size_t referenceCountOffset; +}; + +static void* deletedObject = nullptr; +static int deleterCalls = 0; + +static void APS5_VABI RecordDelete(void* object) { + deletedObject = object; + ++deleterCalls; +} + +static std::int32_t ReadCount(const unsigned char* object, const std::size_t offset) { + std::int32_t value; + std::memcpy(&value, object + offset, sizeof(value)); + return value; +} + +static void WriteCount(unsigned char* object, const std::size_t offset, const std::int32_t value) { + std::memcpy(object + offset, &value, sizeof(value)); +} + +static bool OnlyCountChanged(const unsigned char* object, const std::size_t offset) { + for (std::size_t index = 0; index < ObjectSize; ++index) { + if (index >= offset && index < offset + sizeof(std::int32_t)) continue; + if (object[index] != 0x5a) return false; + } + return true; +} + +static void TestConstruct(const Construct construct) { + IntrusivePtr pointer; + std::memset(&pointer, 0xa5, sizeof(pointer)); + construct(&pointer); + Require(pointer.object == nullptr); + Require(pointer.deleter == nullptr); + Require(pointer.libContext == nullptr); +} + +static void TestAccess(const Access access) { + int object = 0; + int context = 0; + const IntrusivePtr empty{nullptr, RecordDelete, &context}; + Require(access(&empty) == nullptr); + const IntrusivePtr filled{&object, nullptr, nullptr}; + Require(access(&filled) == &object); +} + +static void TestCopy(const Copy copy) { + alignas(8) unsigned char object[ObjectSize]; + std::memset(object, 0x5a, sizeof(object)); + WriteCount(object, 0, 1); + int context = 0; + + const IntrusivePtr source{object, RecordDelete, &context}; + IntrusivePtr target; + std::memset(&target, 0xa5, sizeof(target)); + copy(&target, &source); + Require(target.object == object); + Require(target.deleter == RecordDelete); + Require(target.libContext == &context); + Require(source.object == object); + Require(ReadCount(object, 0) == 2); + Require(OnlyCountChanged(object, 0)); + + const IntrusivePtr empty{nullptr, RecordDelete, &context}; + std::memset(&target, 0xa5, sizeof(target)); + copy(&target, &empty); + Require(target.object == nullptr); + Require(target.deleter == RecordDelete); + Require(target.libContext == &context); +} + +static void TestDestroy(const DestroyCase& testCase) { + alignas(8) unsigned char object[ObjectSize]; + std::memset(object, 0x5a, sizeof(object)); + int context = 0; + const std::size_t offset = testCase.referenceCountOffset; + + deleterCalls = 0; + deletedObject = nullptr; + + IntrusivePtr empty{nullptr, RecordDelete, &context}; + testCase.destroy(&empty); + Require(empty.object == nullptr); + Require(empty.deleter == RecordDelete); + Require(empty.libContext == &context); + Require(deleterCalls == 0); + + WriteCount(object, offset, 2); + IntrusivePtr shared{object, RecordDelete, &context}; + testCase.destroy(&shared); + Require(ReadCount(object, offset) == 1); + Require(shared.object == object); + Require(shared.deleter == RecordDelete); + Require(shared.libContext == &context); + Require(deleterCalls == 0); + + IntrusivePtr last{object, RecordDelete, &context}; + testCase.destroy(&last); + Require(ReadCount(object, offset) == 0); + Require(OnlyCountChanged(object, offset)); + Require(deleterCalls == 1); + Require(deletedObject == object); + Require(last.object == nullptr); + Require(last.deleter == nullptr); + Require(last.libContext == &context); + + WriteCount(object, offset, 1); + IntrusivePtr pooled{object, nullptr, &context}; + bool thrown = false; + try { + testCase.destroy(&pooled); + } catch (const std::runtime_error&) { + thrown = true; + } + Require(thrown); + Require(deleterCalls == 1); +} + +static void TestSharedOwnership() { + alignas(8) unsigned char object[ObjectSize]; + std::memset(object, 0x5a, sizeof(object)); + WriteCount(object, 0, 1); + deleterCalls = 0; + deletedObject = nullptr; + + IntrusivePtr owner; + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEC1Ev(&owner); + owner.object = object; + owner.deleter = RecordDelete; + + IntrusivePtr argument; + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEC1ERS7_(&argument, &owner); + Require(_ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEptEv(&argument) == object); + Require(ReadCount(object, 0) == 2); + + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEED1Ev(&argument); + Require(ReadCount(object, 0) == 1); + Require(deleterCalls == 0); + + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEED1Ev(&owner); + Require(ReadCount(object, 0) == 0); + Require(deleterCalls == 1); + Require(deletedObject == object); + Require(_ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEptEv(&owner) == nullptr); +} + +static void TestVectorPushBackReleasesElements() { + int context = 0; + alignas(8) unsigned char object[ObjectSize]; + std::memset(object, 0x5a, sizeof(object)); + WriteCount(object, 0, 1); + deleterCalls = 0; + deletedObject = nullptr; + + IntrusivePtr vector; + _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V126RequestPlayerSessionPlayerEEEEC1EPNS2_10LibContextE(&vector, &context); + Require(vector.object == nullptr); + Require(vector.deleter == nullptr); + Require(vector.libContext == &context); + + const IntrusivePtr empty{nullptr, nullptr, &context}; + Require(_ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V126RequestPlayerSessionPlayerEEEE8pushBackERKS8_(&vector, &empty) == 0); + Require(vector.object != nullptr); + Require(vector.deleter != nullptr); + + IntrusivePtr filled{object, RecordDelete, &context}; + Require(_ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V126RequestPlayerSessionPlayerEEEE8pushBackERKS8_(&vector, &filled) == 0); + Require(ReadCount(object, 0) == 2); + + _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V126RequestPlayerSessionPlayerEEEED1Ev(&vector); + Require(vector.object == nullptr); + Require(vector.deleter == nullptr); + Require(ReadCount(object, 0) == 1); + Require(deleterCalls == 0); + + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V126RequestPlayerSessionPlayerEED1Ev(&filled); + Require(ReadCount(object, 0) == 0); + Require(deleterCalls == 1); + Require(deletedObject == object); +} + +static void TestAdoptedVectorBodyDestroyedThroughDeleter() { + int context = 0; + alignas(8) unsigned char body[ObjectSize]; + std::memset(body, 0x5a, sizeof(body)); + WriteCount(body, VectorReferenceCountOffset, 1); + deleterCalls = 0; + deletedObject = nullptr; + + IntrusivePtr vector; + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS2_6StringEEEEC1EPS6_PFvS8_EPNS2_10LibContextE(&vector, body, RecordDelete, &context); + Require(vector.object == body); + Require(vector.deleter == RecordDelete); + Require(vector.libContext == &context); + + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS2_6StringEEEED1Ev(&vector); + Require(ReadCount(body, VectorReferenceCountOffset) == 0); + Require(deleterCalls == 1); + Require(deletedObject == body); + Require(vector.object == nullptr); +} + +int main() { + Require(sizeof(IntrusivePtr) == 0x18); + + const Construct constructors[] = { + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEEC1Ev, + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEEC1Ev, + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEC1Ev, + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEC1Ev, + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEEC1Ev, + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V14UserEEC1Ev, + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V19RecordApi26RecordScoreResponseHeadersEEC1Ev, + }; + for (const auto construct : constructors) TestConstruct(construct); + + const Access accessors[] = { + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEEptEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEEptEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEptEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEptEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEEptEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V14UserEEptEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V15EntryEEptEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V19RecordApi26RecordScoreResponseHeadersEEptEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6BinaryEEptEv, + _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_12Leaderboards2V15EntryEEEEEEdeEv, + }; + for (const auto access : accessors) TestAccess(access); + + const Copy copies[] = { + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEEC1ERS7_, + _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEC1ERS7_, + }; + for (const auto copy : copies) TestCopy(copy); + + const DestroyCase destructors[] = { + {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEED1Ev, 0}, + {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEED1Ev, 0}, + {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEED1Ev, 0}, + {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEED1Ev, 0}, + {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEED1Ev, 0}, + {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V14UserEED1Ev, 0}, + {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V19RecordApi26RecordScoreResponseHeadersEED1Ev, 0}, + {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_12Leaderboards2V15EntryEEEEEED1Ev, VectorReferenceCountOffset}, + }; + for (const auto& testCase : destructors) TestDestroy(testCase); + + TestSharedOwnership(); + TestVectorPushBackReleasesElements(); + TestAdoptedVectorBodyDestroyedThroughDeleter(); + return 0; +} diff --git a/core/libs/tests/GuestNpCppWebApiString.cpp b/core/libs/tests/GuestNpCppWebApiString.cpp new file mode 100644 index 0000000000..18a4dd25a6 --- /dev/null +++ b/core/libs/tests/GuestNpCppWebApiString.cpp @@ -0,0 +1,191 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include + +extern "C" { +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6StringC1EPNS2_10LibContextE(void* self, void* libContext); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6StringD1Ev(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6String6appendEPKc(void* self, const char* text); +const char* APS5_VABI _ZNK3sce2Np9CppWebApi6Common6String5c_strEv(const void* self); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEEC1EPNS2_10LibContextE(void* self, void* libContext); +void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEED1Ev(void* self); +int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEE8pushBackERKS4_(void* self, const void* element); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +constexpr std::size_t kStringSize = 0x20; +constexpr std::size_t kGuardSize = 0x10; +constexpr std::size_t kReferenceCountOffset = 0x00; +constexpr std::size_t kBufferOffset = 0x08; +constexpr std::size_t kBufferSizeOffset = 0x10; +constexpr std::size_t kLibContextOffset = 0x18; +constexpr unsigned char kFill = 0xA5; + +constexpr std::size_t kVectorSize = 0x18; +constexpr std::size_t kObjectOffset = 0x00; +constexpr std::size_t kBodyMagicOffset = 0x00; +constexpr std::size_t kBodyReferenceCountOffset = 0x30; +constexpr std::size_t kBodyElementKindOffset = 0x34; +constexpr std::size_t kBodyElementsOffset = 0x38; +constexpr std::size_t kBodySizeOffset = 0x40; + +struct Storage { + alignas(8) unsigned char bytes[kStringSize + kGuardSize]; +}; + +struct VectorStorage { + alignas(8) unsigned char bytes[kVectorSize + kGuardSize]; +}; + +template +TValue Load(const Storage& storage, std::size_t offset) { + TValue value; + std::memcpy(&value, storage.bytes + offset, sizeof(value)); + return value; +} + +template +void Store(Storage& storage, std::size_t offset, TValue value) { + std::memcpy(storage.bytes + offset, &value, sizeof(value)); +} + +template +TValue LoadBody(const void* body, std::size_t offset) { + TValue value; + std::memcpy(&value, static_cast(body) + offset, sizeof(value)); + return value; +} + +bool GuardIntact(const Storage& storage) { + for (std::size_t i = kStringSize; i < sizeof(storage.bytes); i++) { + if (storage.bytes[i] != kFill) return false; + } + return true; +} + +bool VectorGuardIntact(const VectorStorage& storage) { + for (std::size_t i = kVectorSize; i < sizeof(storage.bytes); i++) { + if (storage.bytes[i] != kFill) return false; + } + return true; +} + +void TestConstructedStringIsEmpty() { + std::uint64_t libContext = 1; + Storage storage; + std::memset(storage.bytes, kFill, sizeof(storage.bytes)); + + _ZN3sce2Np9CppWebApi6Common6StringC1EPNS2_10LibContextE(storage.bytes, &libContext); + + Require(Load(storage, kReferenceCountOffset) == 0); + Require(Load(storage, kBufferOffset) == nullptr); + Require(Load(storage, kBufferSizeOffset) == 0); + Require(Load(storage, kLibContextOffset) == &libContext); + Require(GuardIntact(storage)); + + const char* text = _ZNK3sce2Np9CppWebApi6Common6String5c_strEv(storage.bytes); + Require(text != nullptr && text[0] == '\0'); + + _ZN3sce2Np9CppWebApi6Common6StringD1Ev(storage.bytes); + Require(Load(storage, kBufferOffset) == nullptr); + Require(Load(storage, kBufferSizeOffset) == 0); + Require(GuardIntact(storage)); +} + +void TestNullLibContextIsStored() { + Storage storage; + std::memset(storage.bytes, kFill, sizeof(storage.bytes)); + + _ZN3sce2Np9CppWebApi6Common6StringC1EPNS2_10LibContextE(storage.bytes, nullptr); + + Require(Load(storage, kLibContextOffset) == nullptr); + Require(Load(storage, kBufferOffset) == nullptr); + Require(_ZNK3sce2Np9CppWebApi6Common6String5c_strEv(storage.bytes)[0] == '\0'); + _ZN3sce2Np9CppWebApi6Common6StringD1Ev(storage.bytes); +} + +void TestAppendBuildsAndClearsBuffer() { + std::uint64_t libContext = 1; + Storage storage; + std::memset(storage.bytes, kFill, sizeof(storage.bytes)); + + _ZN3sce2Np9CppWebApi6Common6StringC1EPNS2_10LibContextE(storage.bytes, &libContext); + + Require(_ZN3sce2Np9CppWebApi6Common6String6appendEPKc(storage.bytes, "rank") == 0); + Require(std::strcmp(_ZNK3sce2Np9CppWebApi6Common6String5c_strEv(storage.bytes), "rank") == 0); + Require(_ZN3sce2Np9CppWebApi6Common6String6appendEPKc(storage.bytes, "ing") == 0); + Require(std::strcmp(_ZNK3sce2Np9CppWebApi6Common6String5c_strEv(storage.bytes), "ranking") == 0); + Require(Load(storage, kBufferOffset) != nullptr); + Require(GuardIntact(storage)); + + _ZN3sce2Np9CppWebApi6Common6StringD1Ev(storage.bytes); + Require(Load(storage, kBufferOffset) == nullptr); + Require(Load(storage, kBufferSizeOffset) == 0); + Require(GuardIntact(storage)); +} + +void TestAppendNullKeepsEmpty() { + std::uint64_t libContext = 1; + Storage storage; + std::memset(storage.bytes, kFill, sizeof(storage.bytes)); + + _ZN3sce2Np9CppWebApi6Common6StringC1EPNS2_10LibContextE(storage.bytes, &libContext); + Require(_ZN3sce2Np9CppWebApi6Common6String6appendEPKc(storage.bytes, nullptr) == 0); + Require(Load(storage, kBufferOffset) == nullptr); + Require(_ZNK3sce2Np9CppWebApi6Common6String5c_strEv(storage.bytes)[0] == '\0'); + _ZN3sce2Np9CppWebApi6Common6StringD1Ev(storage.bytes); +} + +void TestVectorPushBackCopiesStringContent() { + std::uint64_t libContext = 1; + VectorStorage vector; + Storage element; + std::memset(vector.bytes, kFill, sizeof(vector.bytes)); + std::memset(element.bytes, kFill, sizeof(element.bytes)); + + _ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEEC1EPNS2_10LibContextE(vector.bytes, &libContext); + Require(VectorGuardIntact(vector)); + + _ZN3sce2Np9CppWebApi6Common6StringC1EPNS2_10LibContextE(element.bytes, &libContext); + Require(_ZN3sce2Np9CppWebApi6Common6String6appendEPKc(element.bytes, "privilege") == 0); + char* elementBuffer = Load(element, kBufferOffset); + Require(elementBuffer != nullptr); + + Require(_ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEE8pushBackERKS4_(vector.bytes, element.bytes) == 0); + + void* body; + std::memcpy(&body, vector.bytes + kObjectOffset, sizeof(body)); + Require(body != nullptr); + Require(LoadBody(body, kBodyReferenceCountOffset) == 1); + Require(LoadBody(body, kBodyElementKindOffset) == 2); + Require(LoadBody(body, kBodySizeOffset) == 1); + + void* elements = LoadBody(body, kBodyElementsOffset); + char* storedBuffer; + std::memcpy(&storedBuffer, static_cast(elements) + kBufferOffset, sizeof(storedBuffer)); + Require(storedBuffer != nullptr); + Require(storedBuffer != elementBuffer); + Require(std::strcmp(storedBuffer, "privilege") == 0); + + _ZN3sce2Np9CppWebApi6Common6StringD1Ev(element.bytes); + Require(std::strcmp(storedBuffer, "privilege") == 0); + + _ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEED1Ev(vector.bytes); + Require(VectorGuardIntact(vector)); +} + +} + +int main() { + TestConstructedStringIsEmpty(); + TestNullLibContextIsStored(); + TestAppendBuildsAndClearsBuffer(); + TestAppendNullKeepsEmpty(); + TestVectorPushBackCopiesStringContent(); +} diff --git a/core/libs/tests/GuestNpGameIntent.cpp b/core/libs/tests/GuestNpGameIntent.cpp index 8d63fecbe1..dbf030f79e 100644 --- a/core/libs/tests/GuestNpGameIntent.cpp +++ b/core/libs/tests/GuestNpGameIntent.cpp @@ -14,6 +14,9 @@ int APS5_VABI sceNpGameIntentGetPropertyValueString(const NpGameIntentData* inte int APS5_VABI sceNpGameIntentVshInitialize(void); int APS5_VABI sceNpGameIntentVshTerminate(void); int APS5_VABI sceNpGameIntentTerminate(void); +int APS5_VABI sceNpAppLauncherInitialize(const void* initParam); +int APS5_VABI sceNpAppLauncherLaunchApp(void); +int APS5_VABI sceNpAppLauncherTerminate(void); } static void Require(bool value, const char* what) { @@ -62,6 +65,16 @@ int main() { Require(sceNpGameIntentVshTerminate() == 0, "vsh terminate failed"); Require(sceNpGameIntentTerminate() == 0, "terminate failed"); + unsigned char appLauncherParam[0x28]{}; + *reinterpret_cast(appLauncherParam) = 0x28; + Require(sceNpAppLauncherInitialize(appLauncherParam) == 0, "app launcher init failed"); + Require(Throws([] { sceNpAppLauncherLaunchApp(); }), + "app launcher launch must throw until implemented"); + Require(sceNpAppLauncherTerminate() == 0, "app launcher terminate failed"); + *reinterpret_cast(appLauncherParam) = 0x10; + Require(sceNpAppLauncherInitialize(appLauncherParam) == 0, + "app launcher init must accept any init parameter"); + std::puts("NpGameIntent tests passed"); return 0; } diff --git a/core/libs/tests/GuestNpManager.cpp b/core/libs/tests/GuestNpManager.cpp index 4fe07d41f6..ca4daa5da4 100644 --- a/core/libs/tests/GuestNpManager.cpp +++ b/core/libs/tests/GuestNpManager.cpp @@ -8,7 +8,9 @@ #include extern "C" { +int APS5_VABI sceNpGetAccountCountryA(int user_id, void* country_code); int APS5_VABI sceNpGetNpId(int user_id, NpId* np_id); +int APS5_VABI sceNpGetOnlineId(int user_id, NpOnlineId* online_id); int APS5_VABI sceNpGetUserIdByAccountId(std::uint64_t account_id, int* user_id); int APS5_VABI sceNpUnknown_8weiQyIdI3A(int userId, void* buffer); int APS5_VABI sceNpUnknown_WxLcN76q19o(void); @@ -29,6 +31,7 @@ int APS5_VABI sceNpUnregisterStateCallback(void); int APS5_VABI sceNpRegisterStateCallbackA(void* callback, void* userdata); int APS5_VABI sceNpUnregisterStateCallbackA(int callbackId); int APS5_VABI sceNpRegisterNpReachabilityStateCallback(void* callback, void* userdata); +void APS5_VABI sceNpRegisterGamePresenceCallback(void* callback, void* userdata); int APS5_VABI sceNpUnregisterNpReachabilityStateCallback(void); } @@ -74,6 +77,10 @@ bool SignedOutOnce(const StateCall& call, void* userdata) { call.npId == nullptr && call.userdata == userdata; } +void APS5_VABI AbortingGamePresenceCallback(const void*, const void*, void*) { + std::abort(); +} + void APS5_VABI AbortingReachabilityCallback(int, std::uint32_t, void*) { std::fprintf(stderr, "NpManager: the reachability callback was called\n"); std::abort(); @@ -109,6 +116,8 @@ void CheckStateCallbacksA() { Require(stateCallsA[0].calls == 0, "registering calls the state callback"); Require(sceNpRegisterNpReachabilityStateCallback(reinterpret_cast(&AbortingReachabilityCallback), nullptr) == 0, "registering the reachability callback"); + sceNpRegisterGamePresenceCallback(nullptr, nullptr); + sceNpRegisterGamePresenceCallback(reinterpret_cast(&AbortingGamePresenceCallback), &userdata[0]); Require(sceNpCheckCallback() == 0, "sceNpCheckCallback failed"); Require(SignedOutOnce(stateCallsA[0], &userdata[0]), "sceNpCheckCallback must deliver SIGNED_OUT once"); Require(sceNpUnregisterNpReachabilityStateCallback() == 0, "unregistering the reachability callback"); @@ -213,6 +222,20 @@ int main() { Require(sceNpGetNpId(0x10000, &npId) == SignedOut, "sceNpGetNpId must report the user as signed out"); Require(std::memcmp(&npId, &untouched, sizeof(npId)) == 0, "sceNpGetNpId must leave the NpId untouched"); Require(sceNpGetNpId(0x10000, nullptr) == InvalidArgument, "sceNpGetNpId must reject a null NpId"); + char countryCode[4]{}; + std::memset(countryCode, 0x5a, sizeof(countryCode)); + char untouchedCountryCode[4]{}; + std::memset(untouchedCountryCode, 0x5a, sizeof(untouchedCountryCode)); + Require(sceNpGetAccountCountryA(0x10000, countryCode) == SignedOut, "sceNpGetAccountCountryA must report the user as signed out"); + Require(std::memcmp(countryCode, untouchedCountryCode, sizeof(countryCode)) == 0, "sceNpGetAccountCountryA must leave the country code untouched"); + Require(sceNpGetAccountCountryA(0x10000, nullptr) == InvalidArgument, "sceNpGetAccountCountryA must reject a null country code"); + NpOnlineId onlineId{}; + std::memset(&onlineId, 0x5a, sizeof(onlineId)); + NpOnlineId untouchedOnlineId{}; + std::memset(&untouchedOnlineId, 0x5a, sizeof(untouchedOnlineId)); + Require(sceNpGetOnlineId(0x10000, &onlineId) == SignedOut, "sceNpGetOnlineId must report the user as signed out"); + Require(std::memcmp(&onlineId, &untouchedOnlineId, sizeof(onlineId)) == 0, "sceNpGetOnlineId must leave the online id untouched"); + Require(sceNpGetOnlineId(0x10000, nullptr) == InvalidArgument, "sceNpGetOnlineId must reject a null online id"); int userId = 0x5a5a; Require(sceNpGetUserIdByAccountId(0, &userId) == InvalidArgument, "sceNpGetUserIdByAccountId must reject account id 0"); Require(sceNpGetUserIdByAccountId(0x1234, nullptr) == InvalidArgument, "sceNpGetUserIdByAccountId must reject a null output"); diff --git a/core/libs/tests/GuestNpParty.cpp b/core/libs/tests/GuestNpParty.cpp new file mode 100644 index 0000000000..d47398d76a --- /dev/null +++ b/core/libs/tests/GuestNpParty.cpp @@ -0,0 +1,26 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +extern "C" int APS5_VABI sceNpPartyGetState(std::uint16_t* state); + +int main() { + std::uint16_t states[3]; + std::memset(states, 0xa5, sizeof(states)); + if (sceNpPartyGetState(nullptr) != static_cast(0x80552504u)) { + std::fprintf(stderr, "a null state was not refused with INVALID_ARGUMENT\n"); + return 1; + } + if (sceNpPartyGetState(&states[1]) != 0 || states[1] != 2) { + std::fprintf(stderr, "the state is not NOT_IN_PARTY\n"); + return 1; + } + if (states[0] != 0xa5a5 || states[2] != 0xa5a5) { + std::fprintf(stderr, "sceNpPartyGetState wrote past its 16-bit state\n"); + return 1; + } + std::puts("NpParty state tests passed"); + return 0; +} diff --git a/core/libs/tests/GuestNpTus.cpp b/core/libs/tests/GuestNpTus.cpp index f2376a4113..11d9cf5cf5 100644 --- a/core/libs/tests/GuestNpTus.cpp +++ b/core/libs/tests/GuestNpTus.cpp @@ -1,9 +1,13 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include "SceTypes.hpp" +#include #include #include +#include extern "C" { int APS5_VABI sceNpTusCreateNpTitleCtxA(std::uint32_t serviceLabel, int userId); +int APS5_VABI sceNpTusDeleteNpTitleCtx(int titleCtxId); int APS5_VABI sceNpTusCreateRequest(int titleCtxId); int APS5_VABI sceNpTusDeleteRequest(int reqId); int APS5_VABI sceNpTusAbortRequest(int reqId); @@ -12,10 +16,29 @@ int APS5_VABI sceNpTusGetMultiSlotVariableAAsync(int reqId, std::uint64_t target int APS5_VABI sceNpTusSetMultiSlotVariableAAsync(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, const std::int64_t* variableArray, int arrayLen, void* option); int APS5_VABI sceNpTusDeleteMultiSlotVariableAAsync(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, int arrayLen, void* option); int APS5_VABI sceNpTusAddAndGetVariableAAsync(int reqId, std::uint64_t targetAccountId, std::int32_t slotId, std::int64_t inVariable, const std::uint64_t* isLastChangedAuthor, const void* isLastChangedDate, void* outVariable, std::uint64_t outVariableSize, void* option); +int APS5_VABI sceNpTusWaitAsync(int reqId, int* result); +int APS5_VABI sceNpTusGetMultiSlotVariableA(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotVariableAsync(int reqId, const void* targetNpId, const std::int32_t* slotIds, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotVariableVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotVariableAVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotVariableAVUser(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotVariableForCrossSaveAsync(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotVariableForCrossSave(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotVariableForCrossSaveVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusGetMultiSlotVariableForCrossSaveVUser(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, void* variableArray, std::uint64_t variableArraySize, int arrayLen, void* option); +int APS5_VABI sceNpTusSetMultiSlotVariableA(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, const std::int64_t* variableArray, int arrayLen, void* option); +int APS5_VABI sceNpTusSetMultiSlotVariableAsync(int reqId, const void* targetNpId, const std::int32_t* slotIds, const std::int64_t* variableArray, int arrayLen, void* option); +int APS5_VABI sceNpTusSetMultiSlotVariableVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, const std::int64_t* variableArray, int arrayLen, void* option); +int APS5_VABI sceNpTusDeleteMultiSlotVariableA(int reqId, std::uint64_t targetAccountId, const std::int32_t* slotIds, int arrayLen, void* option); +int APS5_VABI sceNpTusDeleteMultiSlotVariableAsync(int reqId, const void* targetNpId, const std::int32_t* slotIds, int arrayLen, void* option); +int APS5_VABI sceNpTusDeleteMultiSlotVariable(int reqId, const void* targetNpId, const std::int32_t* slotIds, int arrayLen, void* option); +int APS5_VABI sceNpTusDeleteMultiSlotVariableVUserAsync(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, int arrayLen, void* option); +int APS5_VABI sceNpTusDeleteMultiSlotVariableVUser(int reqId, const NpOnlineId* targetVirtualUserId, const std::int32_t* slotIds, int arrayLen, void* option); } static constexpr int SIGNED_OUT = static_cast(0x80550006); static constexpr int INVALID_ARGUMENT = static_cast(0x80550704); +static constexpr int TOO_MANY_OBJECTS = static_cast(0x80550706); static constexpr int ABORTED = static_cast(0x80550707); static constexpr int INSUFFICIENT_ARGUMENT = static_cast(0x8055070C); static constexpr int INVALID_ID = static_cast(0x8055070E); @@ -28,7 +51,136 @@ static void RequireFinished(int reqId, int expected) { Require(sceNpTusPollAsync(reqId, &result) == 0 && result == expected); } +static void CheckMultiSlotVariables() { + const int context = sceNpTusCreateNpTitleCtxA(0, 1); + Require(context > 0); + const int request = sceNpTusCreateRequest(context); + Require(request > 0); + + int result = 0; + Require(sceNpTusWaitAsync(request + 1000, &result) == INVALID_ID); + Require(sceNpTusWaitAsync(request, &result) == INVALID_ARGUMENT); + + std::int32_t slots[2] = {0, 3}; + std::int32_t negative[1] = {-1}; + std::int64_t values[2] = {5, 7}; + std::uint8_t variables[512]; + for (std::uint8_t& byte : variables) byte = 0x5a; + const NpId npId{}; + const NpOnlineId user{{'u', 's', 'e', 'r'}, {}}; + const NpOnlineId empty{}; + + Require(sceNpTusGetMultiSlotVariableAsync(request, nullptr, nullptr, variables, sizeof(variables), 2, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTusGetMultiSlotVariableAsync(request, nullptr, slots, nullptr, sizeof(variables), 2, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTusGetMultiSlotVariableAsync(request, nullptr, slots, variables, sizeof(variables), 0, nullptr) == INVALID_ARGUMENT); + Require(sceNpTusGetMultiSlotVariableAsync(request, nullptr, slots, variables, sizeof(variables), 65, nullptr) == TOO_MANY_SLOTID); + Require(sceNpTusGetMultiSlotVariableAsync(request, nullptr, negative, variables, sizeof(variables), 1, nullptr) == INVALID_ARGUMENT); + Require(sceNpTusGetMultiSlotVariableAsync(request, nullptr, slots, variables, sizeof(variables), 2, variables) == INVALID_ARGUMENT); + Require(sceNpTusWaitAsync(request, &result) == INVALID_ARGUMENT); + Require(sceNpTusGetMultiSlotVariableAsync(request, nullptr, slots, variables, sizeof(variables), 2, nullptr) == 0); + result = 0; + Require(sceNpTusWaitAsync(request, &result) == 0 && result == SIGNED_OUT); + Require(sceNpTusWaitAsync(request, nullptr) == 0); + Require(sceNpTusGetMultiSlotVariableAsync(request, &npId, slots, variables, sizeof(variables), 2, nullptr) == 0); + RequireFinished(request, SIGNED_OUT); + + Require(sceNpTusGetMultiSlotVariableVUserAsync(request, nullptr, slots, variables, sizeof(variables), 2, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTusGetMultiSlotVariableVUserAsync(request, &user, slots, nullptr, sizeof(variables), 2, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTusGetMultiSlotVariableVUserAsync(request, &user, nullptr, variables, sizeof(variables), 2, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTusGetMultiSlotVariableVUserAsync(request, &user, slots, variables, sizeof(variables), 0, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTusGetMultiSlotVariableVUserAsync(request, &user, slots, variables, sizeof(variables), 2, variables) == INVALID_ARGUMENT); + Require(sceNpTusGetMultiSlotVariableVUserAsync(request, &user, slots, variables, sizeof(variables), 65, nullptr) == TOO_MANY_SLOTID); + Require(sceNpTusGetMultiSlotVariableVUserAsync(request, &user, negative, variables, sizeof(variables), 1, nullptr) == INVALID_ARGUMENT); + Require(sceNpTusGetMultiSlotVariableVUserAsync(request, &empty, slots, variables, sizeof(variables), 2, nullptr) == INVALID_ARGUMENT); + Require(sceNpTusGetMultiSlotVariableVUserAsync(request, &user, slots, variables, sizeof(variables), 2, nullptr) == 0); + RequireFinished(request, SIGNED_OUT); + + for (const auto call : {sceNpTusGetMultiSlotVariableAVUserAsync, sceNpTusGetMultiSlotVariableForCrossSaveVUserAsync}) { + Require(call(request, nullptr, slots, variables, sizeof(variables), 2, nullptr) == INSUFFICIENT_ARGUMENT); + Require(call(request, &user, nullptr, variables, sizeof(variables), 2, nullptr) == INSUFFICIENT_ARGUMENT); + Require(call(request, &user, slots, nullptr, sizeof(variables), 2, nullptr) == INSUFFICIENT_ARGUMENT); + Require(call(request, &user, slots, variables, sizeof(variables), 0, nullptr) == INVALID_ARGUMENT); + Require(call(request, &user, slots, variables, sizeof(variables), 65, nullptr) == TOO_MANY_SLOTID); + Require(call(request, &user, negative, variables, sizeof(variables), 1, nullptr) == INVALID_ARGUMENT); + Require(call(request, &empty, slots, variables, sizeof(variables), 65, nullptr) == TOO_MANY_SLOTID); + Require(call(request, &empty, slots, variables, sizeof(variables), 2, nullptr) == INVALID_ARGUMENT); + Require(call(request, &user, slots, variables, sizeof(variables), 2, nullptr) == 0); + RequireFinished(request, SIGNED_OUT); + } + for (const auto call : {sceNpTusGetMultiSlotVariableAVUser, sceNpTusGetMultiSlotVariableForCrossSaveVUser}) { + Require(call(request, &empty, slots, variables, sizeof(variables), 2, nullptr) == INVALID_ARGUMENT); + Require(call(request, &user, slots, variables, sizeof(variables), 2, nullptr) == SIGNED_OUT); + Require(call(request + 1000, &user, slots, variables, sizeof(variables), 2, nullptr) == INVALID_ID); + } + + Require(sceNpTusGetMultiSlotVariableForCrossSaveAsync(request, 0, slots, nullptr, sizeof(variables), 2, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTusGetMultiSlotVariableForCrossSaveAsync(request, 0, slots, variables, sizeof(variables), 0, nullptr) == INVALID_ARGUMENT); + Require(sceNpTusGetMultiSlotVariableForCrossSaveAsync(request, 0, slots, variables, sizeof(variables), 2, nullptr) == 0); + RequireFinished(request, SIGNED_OUT); + for (const auto call : {sceNpTusGetMultiSlotVariableA, sceNpTusGetMultiSlotVariableForCrossSave}) { + Require(call(request, 0, slots, variables, sizeof(variables), 65, nullptr) == TOO_MANY_SLOTID); + Require(call(request, 0, slots, variables, sizeof(variables), 2, nullptr) == SIGNED_OUT); + Require(call(request + 1000, 0, slots, variables, sizeof(variables), 2, nullptr) == INVALID_ID); + } + + Require(sceNpTusSetMultiSlotVariableAsync(request, nullptr, slots, nullptr, 2, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTusSetMultiSlotVariableAsync(request, nullptr, nullptr, values, 2, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTusSetMultiSlotVariableAsync(request, nullptr, slots, values, 0, nullptr) == INVALID_ARGUMENT); + Require(sceNpTusSetMultiSlotVariableAsync(request, nullptr, negative, values, 1, nullptr) == INVALID_ARGUMENT); + Require(sceNpTusSetMultiSlotVariableAsync(request, &npId, slots, values, 2, nullptr) == 0); + RequireFinished(request, SIGNED_OUT); + Require(sceNpTusSetMultiSlotVariableVUserAsync(request, nullptr, slots, values, 2, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTusSetMultiSlotVariableVUserAsync(request, &user, slots, nullptr, 2, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTusSetMultiSlotVariableVUserAsync(request, &user, nullptr, values, 2, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTusSetMultiSlotVariableVUserAsync(request, &user, slots, values, 0, nullptr) == INVALID_ARGUMENT); + Require(sceNpTusSetMultiSlotVariableVUserAsync(request, &user, slots, values, 2, values) == INVALID_ARGUMENT); + Require(sceNpTusSetMultiSlotVariableVUserAsync(request, &user, negative, values, 1, nullptr) == INVALID_ARGUMENT); + Require(sceNpTusSetMultiSlotVariableVUserAsync(request, &user, slots, values, 65, nullptr) == TOO_MANY_SLOTID); + Require(sceNpTusSetMultiSlotVariableVUserAsync(request, &empty, slots, values, 2, nullptr) == INVALID_ARGUMENT); + Require(sceNpTusSetMultiSlotVariableVUserAsync(request, &user, slots, values, 2, nullptr) == 0); + RequireFinished(request, SIGNED_OUT); + Require(sceNpTusSetMultiSlotVariableA(request, 0, slots, nullptr, 2, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTusSetMultiSlotVariableA(request, 0, slots, values, 2, nullptr) == SIGNED_OUT); + + Require(sceNpTusDeleteMultiSlotVariableAsync(request, nullptr, slots, 2, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTusDeleteMultiSlotVariableAsync(request, &npId, nullptr, 2, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTusDeleteMultiSlotVariableAsync(request, &npId, slots, 0, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTusDeleteMultiSlotVariableAsync(request, &npId, slots, 2, values) == INVALID_ARGUMENT); + Require(sceNpTusDeleteMultiSlotVariableAsync(request, &npId, slots, 65, nullptr) == TOO_MANY_SLOTID); + Require(sceNpTusDeleteMultiSlotVariableAsync(request, &npId, negative, 1, nullptr) == INVALID_ARGUMENT); + Require(sceNpTusDeleteMultiSlotVariableAsync(request, &npId, slots, 2, nullptr) == 0); + RequireFinished(request, SIGNED_OUT); + Require(sceNpTusDeleteMultiSlotVariableVUserAsync(request, nullptr, slots, 2, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTusDeleteMultiSlotVariableVUserAsync(request, &user, slots, 0, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTusDeleteMultiSlotVariableVUserAsync(request, &user, nullptr, 2, nullptr) == INSUFFICIENT_ARGUMENT); + Require(sceNpTusDeleteMultiSlotVariableVUserAsync(request, &user, slots, 2, values) == INVALID_ARGUMENT); + Require(sceNpTusDeleteMultiSlotVariableVUserAsync(request, &user, slots, 65, nullptr) == TOO_MANY_SLOTID); + Require(sceNpTusDeleteMultiSlotVariableVUserAsync(request, &user, negative, 1, nullptr) == INVALID_ARGUMENT); + Require(sceNpTusDeleteMultiSlotVariableVUserAsync(request, &empty, slots, 2, nullptr) == INVALID_ARGUMENT); + Require(sceNpTusDeleteMultiSlotVariableVUserAsync(request, &user, slots, 2, nullptr) == 0); + RequireFinished(request, SIGNED_OUT); + Require(sceNpTusDeleteMultiSlotVariable(request, &npId, slots, 2, nullptr) == SIGNED_OUT); + Require(sceNpTusDeleteMultiSlotVariableVUser(request, &user, slots, 2, nullptr) == SIGNED_OUT); + Require(sceNpTusDeleteMultiSlotVariableA(request, 0, slots, 0, nullptr) == INVALID_ARGUMENT); + Require(sceNpTusDeleteMultiSlotVariableA(request, 0, slots, 2, nullptr) == SIGNED_OUT); + + Require(sceNpTusAbortRequest(request) == 0); + Require(sceNpTusDeleteMultiSlotVariable(request, &npId, slots, 2, nullptr) == ABORTED); + Require(sceNpTusGetMultiSlotVariableA(request, 0, slots, variables, sizeof(variables), 2, nullptr) == ABORTED); + + const int fresh = sceNpTusCreateRequest(context); + Require(fresh > 0); + Require(sceNpTusAbortRequest(fresh) == 0); + Require(sceNpTusWaitAsync(fresh, &result) == INVALID_ARGUMENT); + Require(sceNpTusDeleteNpTitleCtx(context) == 0); + Require(sceNpTusWaitAsync(request, &result) == INVALID_ID); + Require(sceNpTusSetMultiSlotVariableA(fresh, 0, slots, values, 2, nullptr) == INVALID_ID); + for (const std::uint8_t byte : variables) Require(byte == 0x5a); +} + int main() { + CheckMultiSlotVariables(); + const int context = sceNpTusCreateNpTitleCtxA(0, 1); Require(context > 0); Require(sceNpTusCreateRequest(context + 1000) == INVALID_ID); @@ -70,4 +222,53 @@ int main() { Require(sceNpTusAbortRequest(request) == INVALID_ID); Require(sceNpTusPollAsync(request, &result) == INVALID_ID); Require(sceNpTusSetMultiSlotVariableAAsync(request, 0, slots, values, 2, nullptr) == INVALID_ID); + + Require(sceNpTusDeleteNpTitleCtx(0) == INVALID_ID); + Require(sceNpTusDeleteNpTitleCtx(-1) == INVALID_ID); + Require(sceNpTusDeleteNpTitleCtx(context + 1000) == INVALID_ID); + const int siblingContext = sceNpTusCreateNpTitleCtxA(0, 1); + const int siblingRequest = sceNpTusCreateRequest(siblingContext); + Require(siblingContext > 0 && siblingRequest > 0); + Require(sceNpTusSetMultiSlotVariableAAsync(siblingRequest, 0, slots, values, 2, nullptr) == 0); + const int idle = sceNpTusCreateRequest(context); + const int finished = sceNpTusCreateRequest(context); + const int aborted = sceNpTusCreateRequest(context); + Require(idle > 0 && finished > 0 && aborted > 0); + Require(sceNpTusSetMultiSlotVariableAAsync(finished, 0, slots, values, 2, nullptr) == 0); + Require(sceNpTusAbortRequest(aborted) == 0); + Require(sceNpTusDeleteNpTitleCtx(context) == 0); + Require(sceNpTusDeleteNpTitleCtx(context) == INVALID_ID); + Require(sceNpTusCreateRequest(context) == INVALID_ID); + for (const int child : {idle, finished, aborted}) { + Require(sceNpTusPollAsync(child, &result) == INVALID_ID); + Require(sceNpTusDeleteRequest(child) == INVALID_ID); + Require(sceNpTusAbortRequest(child) == INVALID_ID); + Require(sceNpTusSetMultiSlotVariableAAsync(child, 0, slots, values, 2, nullptr) == INVALID_ID); + } + RequireFinished(siblingRequest, SIGNED_OUT); + Require(sceNpTusDeleteNpTitleCtx(siblingContext) == 0); + + std::array contexts{}; + for (int& titleContext : contexts) { + titleContext = sceNpTusCreateNpTitleCtxA(0, 1); + Require(titleContext > 0); + } + Require(sceNpTusCreateNpTitleCtxA(0, 1) == TOO_MANY_OBJECTS); + for (int& titleContext : contexts) { + const int previous = titleContext; + Require(sceNpTusDeleteNpTitleCtx(previous) == 0); + titleContext = sceNpTusCreateNpTitleCtxA(0, 1); + Require(titleContext > 0 && titleContext != previous); + Require(sceNpTusCreateRequest(previous) == INVALID_ID); + } + std::array children{}; + for (int& child : children) { + child = sceNpTusCreateRequest(contexts[0]); + Require(child > 0); + } + Require(sceNpTusCreateRequest(contexts[1]) == TOO_MANY_OBJECTS); + Require(sceNpTusDeleteNpTitleCtx(contexts[0]) == 0); + Require(sceNpTusCreateRequest(contexts[1]) > 0); + for (const int child : children) Require(sceNpTusPollAsync(child, &result) == INVALID_ID); + for (std::size_t index = 1; index < contexts.size(); ++index) Require(sceNpTusDeleteNpTitleCtx(contexts[index]) == 0); } diff --git a/core/libs/tests/GuestNpWebApi2.cpp b/core/libs/tests/GuestNpWebApi2.cpp index f6504104c1..fdebb7853e 100644 --- a/core/libs/tests/GuestNpWebApi2.cpp +++ b/core/libs/tests/GuestNpWebApi2.cpp @@ -13,6 +13,9 @@ struct NpWebApi2MemoryPoolStats { extern "C" { int APS5_VABI sceNpWebApi2Initialize(int, std::size_t); +int APS5_VABI sceNpWebApi2InitializeForPresence(int, std::uint64_t); +int APS5_VABI sceNpWebApi2IntInitialize(const NpWebApi2IntInitializeArgs*); +int APS5_VABI sceNpWebApi2IntInitialize2(const NpWebApi2IntInitialize2Args*); int APS5_VABI sceNpWebApi2Terminate(int); int APS5_VABI sceNpWebApi2GetMemoryPoolStats(int, NpWebApi2MemoryPoolStats*); int APS5_VABI sceNpWebApi2PushEventCreateHandle(int); @@ -60,5 +63,23 @@ int main() { Require(Throws([&] { sceNpWebApi2PushEventAbortHandle(context, handle); })); Require(Throws([&] { sceNpWebApi2PushEventSetHandleTimeout(context, handle, 30000); })); Require(sceNpWebApi2Terminate(other) == 0); + + constexpr int invalidArgument = static_cast(0x80553402); + NpWebApi2IntInitializeArgs args{1, 0, 0x4000, "anyps5", sizeof(NpWebApi2IntInitializeArgs)}; + const int internal = sceNpWebApi2IntInitialize(&args); + Require(internal > 0 && sceNpWebApi2GetMemoryPoolStats(internal, &stats) == 0 && stats.pool_size == 0x4000); + args.struct_size = sizeof(NpWebApi2IntInitializeArgs) - 8; + Require(sceNpWebApi2IntInitialize(&args) == invalidArgument && sceNpWebApi2IntInitialize(nullptr) == invalidArgument); + NpWebApi2IntInitialize2Args args2{1, 0, 0x6000, nullptr, 1, 0, sizeof(NpWebApi2IntInitialize2Args)}; + const int internal2 = sceNpWebApi2IntInitialize2(&args2); + Require(internal2 > 0 && internal2 != internal && sceNpWebApi2GetMemoryPoolStats(internal2, &stats) == 0 && stats.pool_size == 0x6000); + args2.struct_size = sizeof(NpWebApi2IntInitializeArgs); + Require(sceNpWebApi2IntInitialize2(&args2) == invalidArgument && sceNpWebApi2IntInitialize2(nullptr) == invalidArgument); + const int presence = sceNpWebApi2InitializeForPresence(1, 0x2000); + Require(presence > 0 && sceNpWebApi2GetMemoryPoolStats(presence, &stats) == 0 && stats.pool_size == 0x2000); + for (const int created : {internal, internal2, presence}) { + Require(sceNpWebApi2Terminate(created) == 0); + Require(Throws([&] { sceNpWebApi2GetMemoryPoolStats(created, &stats); })); + } return 0; } diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index 5d497995b8..335c38cea7 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -3,11 +3,12 @@ #include "prx/libScePad/include/PadState.hpp" #include "prx/libScePad/include/InputCommandFile.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include +#include #include #include -#include #include #include #include @@ -28,20 +29,16 @@ int APS5_VABI scePadResetOrientation(int); int APS5_VABI scePadSetAngularVelocityDeadbandState(int, bool); int APS5_VABI scePadSetAngularVelocityBiasCorrectionState(int, bool); int APS5_VABI scePadIsRemoteController(int, bool*); +int APS5_VABI scePadGetAccessibilityControllerState(int, std::uint32_t*); +int APS5_VABI scePadGetControllerInformation(int, PadControllerInformation*); +int APS5_VABI scePadGetExtControllerInformation(int, void*); +int APS5_VABI scePadResetLightBar(int); +int APS5_VABI scePadSetLightBar(int, const PadLightBarParam*); +int APS5_VABI scePadSetMotionSensorState(int, bool); } static void Require(bool value) { if (!value) std::abort(); } -static void RequireUnknownPadNidNotImplemented() { - try { - scePadUnknown_n3kSX62fgNo(); - } catch (const std::runtime_error& error) { - Require(std::string(error.what()) == "scePadUnknown_n3kSX62fgNo not implemented"); - return; - } - std::abort(); -} - static void CheckInputCommands() { const auto path = std::filesystem::temp_directory_path() / ("anyps5-pad-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); struct Cleanup { @@ -121,7 +118,6 @@ static float SettleOrientationW() { } static void CheckTiltCorrection(int handle) { - RequireUnknownPadNidNotImplemented(); PadInputState tilted; tilted.hasMotion = true; tilted.accel = {9.80665f, 0.0f, 0.0f}; @@ -167,6 +163,46 @@ static void CheckTouchContact() { Pad::ReadState(); } +static void CheckTouchPadInformation(int handle) { + PadControllerInformation info; + std::memset(&info, 0x5a, sizeof(info)); + Require(scePadGetControllerInformation(handle, &info) == PAD_OK); + Require(info.touchPadInfo.resolution.x == kTouchPadWidth); + Require(info.touchPadInfo.resolution.y == kTouchPadHeight); + Require(info.touchPadInfo.pixelDensity == 44.86f); + Require(scePadGetControllerInformation(handle + 1, &info) == PAD_ERROR_INVALID_HANDLE); + + std::array extended{}; + extended.fill(0x5a); + Require(scePadGetExtControllerInformation(handle, extended.data()) == PAD_OK); + PadControllerInformation base{}; + std::memcpy(&base, extended.data(), sizeof(base)); + Require(base.touchPadInfo.resolution.x == kTouchPadWidth); + Require(base.touchPadInfo.resolution.y == kTouchPadHeight); + Require(base.touchPadInfo.pixelDensity == 44.86f); +} + +static void CheckVirtualTouchContact() { + PadInputState left; + left.touchLeft = true; + PadPublishInput_nid_postfix(left); + const auto leftData = Pad::ReadState(); + Require(leftData.touch_data_touch_num == 1); + Require(leftData.touch_data_touch0_x == kTouchPadWidth / 4); + Require(leftData.touch_data_touch0_y == kTouchPadHeight / 2); + + PadInputState right; + right.touchRight = true; + PadPublishInput_nid_postfix(right); + const auto rightData = Pad::ReadState(); + Require(rightData.touch_data_touch_num == 1); + Require(rightData.touch_data_touch0_x == kTouchPadWidth * 3 / 4); + Require(rightData.touch_data_touch0_y == kTouchPadHeight / 2); + + PadPublishInput_nid_postfix(PadInputState{}); + Pad::ReadState(); +} + static void CheckPartialTrigger() { PadInputState partial; partial.buttons = static_cast(Pad::PadButton::L2) | static_cast(Pad::PadButton::R2); @@ -200,12 +236,64 @@ static void CheckReadQueue(int handle) { Require(data[0].buttons == 0); } +static void CheckTapBetweenReads() { + const auto cross = static_cast(Pad::PadButton::Cross); + PadPublishInput_nid_postfix(PadInputState{}); + Pad::ReadState(); + PadInputState pressed; + pressed.buttons = cross; + PadPublishInput_nid_postfix(pressed); + PadPublishInput_nid_postfix(PadInputState{}); + Require((Pad::ReadState().buttons & cross) != 0); + Require((Pad::ReadState().buttons & cross) == 0); + PadPublishInput_nid_postfix(pressed); + Require((Pad::ReadState().buttons & cross) != 0); + Require((Pad::ReadState().buttons & cross) != 0); + PadPublishInput_nid_postfix(PadInputState{}); + Require((Pad::ReadState().buttons & cross) == 0); +} + +static void CheckLatchedPressLifetime(int handle) { + const auto cross = static_cast(Pad::PadButton::Cross); + const auto circle = static_cast(Pad::PadButton::Circle); + PadInputState pressed; + PadData data{}; + PadData drained[64]{}; + scePadRead_nid_postfix(handle, drained, 64); + pressed.buttons = cross; + PadPublishInput_nid_postfix(pressed); + PadPublishInput_nid_postfix(PadInputState{}); + Require(scePadRead_nid_postfix(handle, drained, 64) >= 1); + Require(scePadReadState(handle, &data) == PAD_OK); + Require((data.buttons & cross) == 0); + pressed.buttons = cross; + PadPublishInput_nid_postfix(pressed); + PadPublishInput_nid_postfix(PadInputState{}); + std::this_thread::sleep_for(std::chrono::milliseconds(150)); + Require(scePadReadState(handle, &data) == PAD_OK); + Require((data.buttons & cross) == 0); + pressed.buttons = cross; + PadPublishInput_nid_postfix(pressed); + PadPublishInput_nid_postfix(PadInputState{}); + std::this_thread::sleep_for(std::chrono::milliseconds(150)); + pressed.buttons = circle; + PadPublishInput_nid_postfix(pressed); + PadPublishInput_nid_postfix(PadInputState{}); + Require(scePadReadState(handle, &data) == PAD_OK); + Require((data.buttons & cross) == 0); + Require((data.buttons & circle) != 0); +} + static void CheckReadStateHandle(int handle) { PadData data{}; Require(scePadReadState(0, &data) == PAD_ERROR_INVALID_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) { @@ -217,6 +305,15 @@ static void CheckRemoteController(int handle) { Require(!remote); } +static void CheckAccessibilityControllerState(int handle) { + std::uint32_t state = 7; + Require(scePadGetAccessibilityControllerState(handle + 1, nullptr) == PAD_ERROR_INVALID_ARG); + Require(scePadGetAccessibilityControllerState(handle + 1, &state) == PAD_ERROR_INVALID_HANDLE); + Require(state == 7); + Require(scePadGetAccessibilityControllerState(handle, &state) == PAD_OK); + Require(state == 0); +} + static void CheckInjectedState(const std::filesystem::path& path) { PadInputState physical; physical.buttons = 0x2000; @@ -257,7 +354,7 @@ static void CheckInjectedState(const std::filesystem::path& path) { const auto centred = Pad::ReadState(); Require(centred.left_stick_x == 42 && centred.right_stick_y == 17); Require(centred.timestamp > moved.timestamp); - for (const auto& [command, x] : {std::pair{"3 2097152 300\n", 480}, std::pair{"4 4194304 300\n", 1440}}) { + for (const auto& [command, x] : {std::pair{"3 2097152 300\n", kTouchPadWidth / 4}, std::pair{"4 4194304 300\n", kTouchPadWidth * 3 / 4}}) { std::ofstream(path) << command; PadData touched{}; const auto touchDeadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); @@ -266,7 +363,7 @@ static void CheckInjectedState(const std::filesystem::path& path) { if (touched.touch_data_touch_num != 0) break; std::this_thread::sleep_for(std::chrono::milliseconds(10)); } while (std::chrono::steady_clock::now() < touchDeadline); - Require(touched.touch_data_touch_num == 1 && touched.touch_data_touch0_x == x && touched.touch_data_touch0_y == 471); + Require(touched.touch_data_touch_num == 1 && touched.touch_data_touch0_x == x && touched.touch_data_touch0_y == kTouchPadHeight / 2); Require(touched.buttons == (0x2000 | 0x100000)); std::this_thread::sleep_for(std::chrono::milliseconds(310)); const auto lifted = Pad::ReadState(); @@ -277,6 +374,10 @@ static void CheckInjectedState(const std::filesystem::path& path) { int main() { CheckInputCommands(); + std::uint32_t accessibilityState = 7; + Require(scePadGetAccessibilityControllerState(PAD_HANDLE, nullptr) == PAD_ERROR_NOT_INITIALIZED); + Require(scePadGetAccessibilityControllerState(PAD_HANDLE, &accessibilityState) == PAD_ERROR_NOT_INITIALIZED); + Require(accessibilityState == 7); Require(scePadUnknown_n3kSX62fgNo() == static_cast(0x80920005)); const auto path = std::filesystem::temp_directory_path() / ("anyps5-pad-state-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); struct Cleanup { @@ -328,9 +429,10 @@ int main() { Require(scePadOpen_nid_postfix(user, PAD_PORT_TYPE_SPECIAL, 0, nullptr) == static_cast(0x80920007)); Require(scePadGetHandle(user, 0, 0) == noHandle); Require(scePadInit_nid_postfix() == 0); - Require(scePadUnknown_n3kSX62fgNo() == static_cast(0x809200ff)); + Require(scePadUnknown_n3kSX62fgNo() == PAD_OK); const int handle = scePadOpen_nid_postfix(user, 0, 0, nullptr); Require(handle > 0); + Require(scePadUnknown_n3kSX62fgNo() == PAD_OK); Require(scePadGetHandle(user, 0, 0) == handle); Require(scePadOpen_nid_postfix(user, PAD_PORT_TYPE_SPECIAL, 0, nullptr) == static_cast(0x80920007)); Require(scePadGetHandle(user, PAD_PORT_TYPE_SPECIAL, 0) == noHandle); @@ -338,11 +440,25 @@ int main() { CheckTiltCorrection(handle); CheckAngularVelocityDeadband(handle); CheckTouchContact(); + CheckVirtualTouchContact(); + CheckTouchPadInformation(handle); CheckPartialTrigger(); CheckReadQueue(handle); + CheckTapBetweenReads(); + CheckLatchedPressLifetime(handle); CheckReadStateHandle(handle); CheckRemoteController(handle); + CheckAccessibilityControllerState(handle); CheckInjectedState(commandPath); + PadLightBarParam lightBar{255, 128, 64}; + Require(scePadSetLightBar(handle, &lightBar) == PAD_OK); + Require(scePadSetLightBar(handle, nullptr) == PAD_ERROR_INVALID_ARG); + Require(scePadSetLightBar(handle + 1, &lightBar) == PAD_ERROR_INVALID_HANDLE); + Require(scePadResetLightBar(handle) == PAD_OK); + Require(scePadResetLightBar(handle + 1) == PAD_ERROR_INVALID_HANDLE); + Require(scePadSetMotionSensorState(handle, true) == PAD_OK); + Require(scePadSetMotionSensorState(handle, false) == PAD_OK); + Require(scePadSetMotionSensorState(handle + 1, true) == PAD_ERROR_INVALID_HANDLE); Require(scePadSetVibrationMode(handle, 1) == 0); Require(scePadSetVibrationMode(handle, 2) == 0); Require(scePadSetVibrationMode(handle, 3) == 0); @@ -355,6 +471,7 @@ int main() { Require(scePadGetHandle(user, 0, 1) == noHandle); Require(scePadClose_nid_postfix(handle) == 0); Require(scePadGetHandle(user, 0, 0) == noHandle); + Require(scePadUnknown_n3kSX62fgNo() == PAD_OK); Require(scePadSetVibrationTriggerEffectWeakWhileEmbeddedMicInUse(true) == 0); Require(scePadSetAngularVelocityDeadbandState(handle, false) == 0); Require(scePadSetAngularVelocityDeadbandState(handle + 1, false) == PAD_ERROR_INVALID_HANDLE); diff --git a/core/libs/tests/GuestPathErrors.cpp b/core/libs/tests/GuestPathErrors.cpp new file mode 100644 index 0000000000..3da76d9739 --- /dev/null +++ b/core/libs/tests/GuestPathErrors.cpp @@ -0,0 +1,145 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/File/include/FileFlags.hpp" +#include "prx/libkernel/KernelErrors.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceKernelOpen(const char* path, int flags, std::uint16_t mode); +int APS5_VABI sceKernelUnlink(const char* path); +int APS5_VABI sceKernelClose(int d); +int APS5_VABI open_nid_postfix(const char* path, int flags, int mode); +int APS5_VABI unlink_nid_postfix(const char* path); +int APS5_VABI sceKernelStat(const char* path, FileStat* sb); +int APS5_VABI stat_nid_postfix(const char* path, FileStat* sb); +int APS5_VABI lstat_nid_postfix(const char* path, FileStat* sb); +int APS5_VABI sceKernelMkdir(const char* path, std::uint16_t mode); +int APS5_VABI sceKernelRmdir(const char* path); +int APS5_VABI mkdir_nid_postfix(const char* path, std::uint16_t mode); +int APS5_VABI rmdir_nid_postfix(const char* path); +int APS5_VABI sceKernelRename(const char* from, const char* to); +int APS5_VABI rename_nid_postfix(const char* from, const char* to); +int APS5_VABI sceKernelTruncate_nid_postfix(const char* path, std::int64_t length); +int APS5_VABI truncate_nid_postfix(const char* path, std::int64_t length); +int* APS5_VABI __error_nid_postfix(); +} + +namespace { + +void Require(bool value, int line) { + if (!value) { + std::fprintf(stderr, "path error check failed at line %d\n", line); + std::exit(1); + } +} +#define Check(value) Require((value), __LINE__) + +constexpr int GuestEloop = 62; +constexpr int GuestEnametoolong = 63; +constexpr int SceEloop = SceKernelError(GuestEloop); +constexpr int SceEnametoolong = SceKernelError(GuestEnametoolong); + +bool PosixFails(int result, int error) { + const bool failed = result == -1 && *__error_nid_postfix() == error; + *__error_nid_postfix() = 0; + return failed; +} + +std::filesystem::path EnterFreshRoot() { + const auto root = std::filesystem::temp_directory_path() / + ("anyps5-path-errors-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + std::filesystem::create_directories(root); + std::filesystem::current_path(root); + Check(::symlink("loop", "loop") == 0); + Check(::symlink("second", "first") == 0 && ::symlink("first", "second") == 0); + { std::ofstream file("plain.txt"); file << "plain"; } + return root; +} + +const std::string LongName(300, 'n'); + +void OpenAndUnlink() { + Check(sceKernelOpen("loop", SCE_KERNEL_O_RDONLY, 0) == SceEloop); + Check(sceKernelOpen("first", SCE_KERNEL_O_RDONLY, 0) == SceEloop); + Check(sceKernelOpen("loop/inside", SCE_KERNEL_O_RDONLY, 0) == SceEloop); + Check(sceKernelOpen(LongName.c_str(), SCE_KERNEL_O_RDONLY, 0) == SceEnametoolong); + Check(sceKernelOpen((LongName + "/inside").c_str(), SCE_KERNEL_O_RDONLY, 0) == SceEnametoolong); + Check(sceKernelOpen(LongName.c_str(), SCE_KERNEL_O_WRONLY | SCE_KERNEL_O_CREAT, 0644) == SceEnametoolong); + Check(PosixFails(open_nid_postfix("loop", SCE_KERNEL_O_RDONLY, 0), GuestEloop)); + Check(PosixFails(open_nid_postfix(LongName.c_str(), SCE_KERNEL_O_RDONLY, 0), GuestEnametoolong)); + Check(sceKernelUnlink(LongName.c_str()) == SceEnametoolong); + Check(sceKernelUnlink("loop/inside") == SceEloop); + Check(PosixFails(unlink_nid_postfix(LongName.c_str()), GuestEnametoolong)); + const int plain = sceKernelOpen("plain.txt", SCE_KERNEL_O_RDONLY, 0); + Check(plain >= 0 && sceKernelClose(plain) == 0); +} + +void Status() { + FileStat status{}; + Check(sceKernelStat("loop", &status) == SceEloop); + Check(sceKernelStat("first", &status) == SceEloop); + Check(sceKernelStat("loop/inside", &status) == SceEloop); + Check(sceKernelStat(LongName.c_str(), &status) == SceEnametoolong); + Check(sceKernelStat((LongName + "/inside").c_str(), &status) == SceEnametoolong); + Check(PosixFails(stat_nid_postfix("loop", &status), GuestEloop)); + Check(PosixFails(stat_nid_postfix(LongName.c_str(), &status), GuestEnametoolong)); + Check(PosixFails(lstat_nid_postfix(LongName.c_str(), &status), GuestEnametoolong)); + Check(lstat_nid_postfix("loop", &status) == 0 && (status.st_mode & 0170000) == 0120000); + Check(sceKernelStat("plain.txt", &status) == 0 && status.st_size == 5); + Check(sceKernelStat("missing.txt", &status) == SceKernelError(2)); +} + +void Directories() { + Check(sceKernelMkdir("loop/inside", 0755) == SceEloop); + Check(sceKernelMkdir(LongName.c_str(), 0755) == SceEnametoolong); + Check(PosixFails(mkdir_nid_postfix("loop/inside", 0755), GuestEloop)); + Check(sceKernelRmdir("loop/inside") == SceEloop); + Check(sceKernelRmdir(LongName.c_str()) == SceEnametoolong); + Check(sceKernelRmdir((LongName + "/inside").c_str()) == SceEnametoolong); + Check(PosixFails(rmdir_nid_postfix(LongName.c_str()), GuestEnametoolong)); + Check(sceKernelRmdir("loop") == SceKernelError(20)); + Check(sceKernelMkdir("made", 0755) == 0 && sceKernelRmdir("made") == 0); + Check(sceKernelRmdir("made") == SceKernelError(2)); +} + +void Rename() { + Check(sceKernelRename(LongName.c_str(), "target.txt") == SceEnametoolong); + Check(sceKernelRename("loop/inside", "target.txt") == SceEloop); + Check(sceKernelRename("plain.txt", LongName.c_str()) == SceEnametoolong); + Check(sceKernelRename("plain.txt", "loop/inside") == SceEloop); + Check(PosixFails(rename_nid_postfix(LongName.c_str(), "target.txt"), GuestEnametoolong)); + Check(sceKernelRename("missing.txt", "target.txt") == SceKernelError(2)); + Check(sceKernelRename("plain.txt", "moved.txt") == 0 && sceKernelRename("moved.txt", "plain.txt") == 0); +} + +void Truncate() { + Check(sceKernelTruncate_nid_postfix("loop", 0) == SceEloop); + Check(sceKernelTruncate_nid_postfix(LongName.c_str(), 0) == SceEnametoolong); + Check(PosixFails(truncate_nid_postfix("loop", 0), GuestEloop)); + Check(PosixFails(truncate_nid_postfix(LongName.c_str(), 0), GuestEnametoolong)); + Check(sceKernelTruncate_nid_postfix("missing.txt", 0) == SceKernelError(2)); + Check(sceKernelTruncate_nid_postfix("plain.txt", 2) == 0 && std::filesystem::file_size("plain.txt") == 2); +} + +} + +int main() { + const auto root = EnterFreshRoot(); + OpenAndUnlink(); + Status(); + Directories(); + Rename(); + Truncate(); + std::error_code error; + std::filesystem::current_path(std::filesystem::temp_directory_path(), error); + std::filesystem::remove_all(root, error); + return 0; +} 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..590e20232f 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; } @@ -212,10 +221,10 @@ static void RunLayout(const char* self, const char* mode, std::initializer_list< Require(status == 0); } -static std::string ChunkTableFile() { +static std::string ChunkTableFile(const char* magic = "plgx", char chunks = 3) { std::string header(0x100, '\0'); - header.replace(0, 4, "plgx"); - header[0x0A] = 3; + header.replace(0, 4, magic); + header[0x0A] = chunks; return header; } @@ -237,10 +246,12 @@ int main(int argc, char** argv) { RunLayout(argv[0], "--no-file-absent", {}); RunLayout(argv[0], "--no-file-unknown-answer", {}); RunLayout(argv[0], "--package-chunk-table", {{"playgo-chunk.dat", ChunkTableFile()}}); + RunLayout(argv[0], "--package-chunk-table", {{"playgo-chunk.dat", ChunkTableFile("plgo")}}); RunLayout(argv[0], "--scenario-with-chunk-table", {{"playgo-chunk.dat", ChunkTableFile()}, {"playgo-scenario.json", "{}"}}); RunLayout(argv[0], "--scenario-only", {{"playgo-scenario.json", "{}"}}); RunLayout(argv[0], "--ficm-only", {{"playgo-ficm.dat", "x"}}); RunLayout(argv[0], "--hash-table-only", {{"playgo-hash-table.dat", "x"}}); RunLayout(argv[0], "--invalid-chunk-table", {{"playgo-chunk.dat", "not a chunk table"}}); + RunLayout(argv[0], "--invalid-chunk-table", {{"playgo-chunk.dat", ChunkTableFile("plgo", 0)}}); return 0; } diff --git a/core/libs/tests/GuestPlayGoDialog.cpp b/core/libs/tests/GuestPlayGoDialog.cpp index f66e459c7c..470cfee79d 100644 --- a/core/libs/tests/GuestPlayGoDialog.cpp +++ b/core/libs/tests/GuestPlayGoDialog.cpp @@ -21,6 +21,7 @@ int main() { constexpr std::int32_t alreadyInitialized = static_cast(0x80ED0002); constexpr std::int32_t paramInvalid = static_cast(0x80ED0003); constexpr std::int32_t invalidState = static_cast(0x80ED0005); + constexpr std::int32_t notFinished = static_cast(0x80B80005u); Require(scePlayGoDialogGetStatus() == 0); Require(scePlayGoDialogTerminate() == notInitialized); @@ -37,6 +38,9 @@ int main() { Require(scePlayGoDialogOpen(nullptr) == paramInvalid); Require(scePlayGoDialogUpdateStatus() == 1); + PlayGoDialogResult unfinishedResult{}; + Require(scePlayGoDialogGetResult(&unfinishedResult) == notFinished); + Require(scePlayGoDialogOpen(&openParam) == 0); Require(scePlayGoDialogGetStatus() == 3); Require(scePlayGoDialogUpdateStatus() == 3); diff --git a/core/libs/tests/GuestPlayerReviewDialog.cpp b/core/libs/tests/GuestPlayerReviewDialog.cpp index 3287ddd4d9..c7917268a4 100644 --- a/core/libs/tests/GuestPlayerReviewDialog.cpp +++ b/core/libs/tests/GuestPlayerReviewDialog.cpp @@ -46,6 +46,7 @@ int main() { Require(scePlayerReviewDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); Require(scePlayerReviewDialogInitialize() == 0); + Require(scePlayerReviewDialogUpdateStatus() == COMMON_DIALOG_STATUS_INITIALIZED); Require(scePlayerReviewDialogOpen(nullptr) == COMMON_DIALOG_ERROR_ARG_NULL); Require(scePlayerReviewDialogOpen(param) == 0); Require(scePlayerReviewDialogGetStatus() == COMMON_DIALOG_STATUS_FINISHED); @@ -54,8 +55,18 @@ int main() { Require(scePlayerReviewDialogGetResult(result) == 0); Require(result[0] == COMMON_DIALOG_RESULT_USER_CANCELED); Require(scePlayerReviewDialogClose() == 0); + Require(scePlayerReviewDialogGetStatus() == COMMON_DIALOG_STATUS_FINISHED); + Require(scePlayerReviewDialogUpdateStatus() == COMMON_DIALOG_STATUS_FINISHED); Require(scePlayerReviewDialogTerminate() == 0); Require(scePlayerReviewDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); Require(scePlayerReviewDialogInitialize() == 0); + Require(scePlayerReviewDialogGetStatus() == COMMON_DIALOG_STATUS_INITIALIZED); + Require(scePlayerReviewDialogClose() == 0); + Require(scePlayerReviewDialogGetStatus() == COMMON_DIALOG_STATUS_FINISHED); + Require(scePlayerReviewDialogUpdateStatus() == COMMON_DIALOG_STATUS_FINISHED); + result[0] = -1; + Require(scePlayerReviewDialogGetResult(result) == 0); + Require(result[0] == COMMON_DIALOG_RESULT_USER_CANCELED); Require(scePlayerReviewDialogTerminate() == 0); + Require(scePlayerReviewDialogUpdateStatus() == COMMON_DIALOG_STATUS_NONE); } diff --git a/core/libs/tests/GuestPlayerSelectionDialog.cpp b/core/libs/tests/GuestPlayerSelectionDialog.cpp index 639a8297fe..8814165e4f 100644 --- a/core/libs/tests/GuestPlayerSelectionDialog.cpp +++ b/core/libs/tests/GuestPlayerSelectionDialog.cpp @@ -1,6 +1,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include extern "C" { int APS5_VABI scePlayerSelectionDialogInitialize(void); @@ -8,6 +9,7 @@ int APS5_VABI scePlayerSelectionDialogOpen(const void* param); int APS5_VABI scePlayerSelectionDialogUpdateStatus(void); int APS5_VABI scePlayerSelectionDialogGetStatus(void); int APS5_VABI scePlayerSelectionDialogGetResult(void* result); +int APS5_VABI scePlayerSelectionDialogParamInitialize(void* param); int APS5_VABI scePlayerSelectionDialogClose(void); int APS5_VABI scePlayerSelectionDialogTerminate(void); } @@ -32,6 +34,15 @@ int main() { Require(scePlayerSelectionDialogTerminate() == kErrNotInitialized); Require(scePlayerSelectionDialogOpen(¶m) == kErrNotInitialized); Require(scePlayerSelectionDialogClose() == kErrNotInitialized); + Require(scePlayerSelectionDialogParamInitialize(nullptr) == kErrArgNull); + bool threw = false; + try { + scePlayerSelectionDialogParamInitialize(¶m); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); + Require(param == 0); Require(scePlayerSelectionDialogGetResult(result) == kErrNotInitialized); Require(scePlayerSelectionDialogGetStatus() == 0); Require(scePlayerSelectionDialogUpdateStatus() == 0); diff --git a/core/libs/tests/GuestPngDec.cpp b/core/libs/tests/GuestPngDec.cpp index f9536a50a2..83281c1c1c 100644 --- a/core/libs/tests/GuestPngDec.cpp +++ b/core/libs/tests/GuestPngDec.cpp @@ -1,6 +1,7 @@ #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include "Decoder/Png.hpp" +#include #include #include #include @@ -51,6 +52,12 @@ static std::vector RgbPng() { return Decoder::Png::Encode(rgb, 3, 2, 3); } +static std::vector RgbPngWidth(std::uint32_t width) { + std::vector rgb(static_cast(width) * 3); + for (std::size_t i = 0; i < rgb.size(); ++i) rgb[i] = static_cast(i * 17 + 3); + return Decoder::Png::Encode(rgb, width, 1, 3); +} + static PngDecDecodeParam DecodeParam(const std::vector& png, std::vector& image) { PngDecDecodeParam param{}; param.png_mem_addr = png.data(); @@ -81,10 +88,12 @@ int main() { const PngDecCreateParam badAttribute{sizeof(PngDecCreateParam), 2, 1920}; Require(scePngDecQueryMemorySize(&badAttribute) == invalidParam); const PngDecCreateParam zeroWidth{sizeof(PngDecCreateParam), 0, 0}; - Require(scePngDecQueryMemorySize(&zeroWidth) == invalidSize); + Require(scePngDecQueryMemorySize(&zeroWidth) == invalidParam); const PngDecCreateParam hugeWidth{sizeof(PngDecCreateParam), 0, 1000002}; - Require(scePngDecQueryMemorySize(&hugeWidth) == invalidSize); - const PngDecCreateParam maxWidth{sizeof(PngDecCreateParam), 1, 1000001}; + Require(scePngDecQueryMemorySize(&hugeWidth) == invalidParam); + const PngDecCreateParam overWidth{sizeof(PngDecCreateParam), 1, 1000001}; + Require(scePngDecQueryMemorySize(&overWidth) == invalidParam); + const PngDecCreateParam maxWidth{sizeof(PngDecCreateParam), 1, 1000000}; Require(scePngDecQueryMemorySize(&maxWidth) == memorySize); std::vector memory(static_cast(memorySize) + 1); @@ -92,6 +101,7 @@ int main() { void* handle = nullptr; Require(scePngDecCreate(nullptr, unaligned, static_cast(memorySize), &handle) == invalidParam); Require(scePngDecCreate(&badSize, unaligned, static_cast(memorySize), &handle) == invalidSize); + Require(scePngDecCreate(&overWidth, unaligned, static_cast(memorySize), &handle) == invalidParam); Require(scePngDecCreate(¶m, nullptr, static_cast(memorySize), &handle) == invalidAddr); Require(scePngDecCreate(¶m, unaligned, static_cast(memorySize), nullptr) == invalidAddr); Require(scePngDecCreate(¶m, unaligned, static_cast(memorySize) - 1, &handle) == invalidWorkMemory); @@ -135,8 +145,27 @@ int main() { Require(image[pixel * 4 + 3] == 0x77); } + const PngDecCreateParam narrowParam{sizeof(PngDecCreateParam), 0, 9}; + std::vector narrowMemory(static_cast(memorySize)); + void* narrowHandle = nullptr; + Require(scePngDecCreate(&narrowParam, narrowMemory.data(), static_cast(memorySize), &narrowHandle) == 0); + const std::vector alignedWidthPng = RgbPngWidth(16); + std::vector alignedWidthImage(16 * 4, 0xCC); + PngDecDecodeParam narrowDecode = DecodeParam(alignedWidthPng, alignedWidthImage); + Require(scePngDecDecode(narrowHandle, &narrowDecode, &info) == (16 << 16 | 1)); + Require(info.image_width == 16 && info.image_height == 1); + + const std::vector oversizedWidthPng = RgbPngWidth(17); + std::vector oversizedWidthImage(17 * 4, 0xCC); + narrowDecode = DecodeParam(oversizedWidthPng, oversizedWidthImage); + info = {}; + Require(scePngDecDecode(narrowHandle, &narrowDecode, &info) == invalidWorkMemory); + Require(info.image_width == 17 && info.image_height == 1); + Require(std::all_of(oversizedWidthImage.begin(), oversizedWidthImage.end(), [](std::uint8_t byte) { return byte == 0xCC; })); + Require(scePngDecDelete(narrowHandle) == 0); + constexpr std::uint32_t pitch = 3 * 4 + 8; - std::vector padded(pitch + 3 * 4, 0xEE); + std::vector padded(pitch * 2, 0xEE); decode = DecodeParam(rgbPng, padded); decode.pixel_format = 1; decode.image_pitch = pitch; @@ -151,6 +180,23 @@ int main() { } for (std::size_t x = 3 * 4; x < pitch; ++x) Require(padded[x] == 0xEE); + std::vector invalidPitchImage(3 * 2 * 4 + 8, 0xCC); + decode = DecodeParam(rgbPng, invalidPitchImage); + decode.image_pitch = 3 * 4 + 2; + Require(scePngDecDecode(handle, &decode, &info) == invalidParam); + Require(std::all_of(invalidPitchImage.begin(), invalidPitchImage.end(), [](std::uint8_t byte) { return byte == 0xCC; })); + + decode = DecodeParam(rgbPng, invalidPitchImage); + decode.image_pitch = pitch; + decode.image_mem_size = pitch + 3 * 4; + Require(scePngDecDecode(handle, &decode, &info) == invalidParam); + Require(std::all_of(invalidPitchImage.begin(), invalidPitchImage.end(), [](std::uint8_t byte) { return byte == 0xCC; })); + + std::vector fullPitchImage(pitch * 2, 0xCC); + decode = DecodeParam(rgbPng, fullPitchImage); + decode.image_pitch = pitch; + Require(scePngDecDecode(handle, &decode, &info) == (3 << 16 | 2)); + const std::vector palettePng(PALETTE_TRNS, PALETTE_TRNS + sizeof(PALETTE_TRNS)); std::vector paletteImage(2 * 2 * 4); decode = DecodeParam(palettePng, paletteImage); @@ -173,12 +219,26 @@ int main() { decode = DecodeParam(rgbPng, image); decode.image_mem_addr = nullptr; Require(scePngDecDecode(handle, &decode, &info) == invalidAddr); + std::vector misalignedImage(3 * 2 * 4 + 1, 0xCC); + decode = DecodeParam(rgbPng, misalignedImage); + decode.image_mem_addr = misalignedImage.data() + 1; + decode.image_mem_size = static_cast(misalignedImage.size() - 1); + Require(scePngDecDecode(handle, &decode, &info) == invalidAddr); + Require(std::all_of(misalignedImage.begin(), misalignedImage.end(), [](std::uint8_t byte) { return byte == 0xCC; })); decode = DecodeParam(rgbPng, image); decode.png_mem_size = 0; Require(scePngDecDecode(handle, &decode, &info) == invalidSize); decode = DecodeParam(rgbPng, image); decode.pixel_format = 2; Require(scePngDecDecode(handle, &decode, &info) == invalidParam); + std::vector invalidAlphaImage(3 * 2 * 4, 0xCC); + decode = DecodeParam(rgbPng, invalidAlphaImage); + decode.alpha_value = 0x100; + Require(scePngDecDecode(handle, &decode, &info) == invalidParam); + Require(std::all_of(invalidAlphaImage.begin(), invalidAlphaImage.end(), [](std::uint8_t byte) { return byte == 0xCC; })); + decode.alpha_value = 0xFFFF; + Require(scePngDecDecode(handle, &decode, &info) == invalidParam); + Require(std::all_of(invalidAlphaImage.begin(), invalidAlphaImage.end(), [](std::uint8_t byte) { return byte == 0xCC; })); decode = DecodeParam(rgbPng, image); decode.image_pitch = 3 * 4 - 1; Require(scePngDecDecode(handle, &decode, &info) == invalidParam); diff --git a/core/libs/tests/GuestPosixScalarIo.cpp b/core/libs/tests/GuestPosixScalarIo.cpp index 90cafe8ea9..ec96da483a 100644 --- a/core/libs/tests/GuestPosixScalarIo.cpp +++ b/core/libs/tests/GuestPosixScalarIo.cpp @@ -169,7 +169,7 @@ int main() { Require(sceKernelMkdir(nullptr, 0777) == static_cast(0x8002000eu)); Require(sceKernelRmdir(nullptr) == static_cast(0x8002000eu)); Require(sceKernelRename(nullptr, "dest") == static_cast(0x8002000eu)); - Require(sceKernelRename("src", nullptr) == static_cast(0x8002000eu)); + Require(sceKernelRename("src", nullptr) == static_cast(0x80020002u)); Require(sceKernelRename(nullptr, nullptr) == static_cast(0x8002000eu)); Require(close_nid_postfix(descriptor) == 0); const int readOnly = open_nid_postfix(fileName.c_str(), SCE_KERNEL_O_RDONLY, 0); diff --git a/core/libs/tests/GuestProcessTime.cpp b/core/libs/tests/GuestProcessTime.cpp index 145afbc29a..cfff04c3bd 100644 --- a/core/libs/tests/GuestProcessTime.cpp +++ b/core/libs/tests/GuestProcessTime.cpp @@ -24,8 +24,12 @@ int main(int argc, char** argv) { while (std::chrono::steady_clock::now() < slept) std::this_thread::sleep_until(slept); if (argc > 1 && std::strcmp(argv[1], "counter") == 0) { const std::uint64_t counter = sceKernelGetProcessTimeCounter(); - Require(counter < minuteMicros * 1000, "first process time counter wrapped"); - Require(counter >= minimumElapsedMicros * 1000, "first process time counter does not include the time before the first call"); + const std::uint64_t frequency = sceKernelGetProcessTimeCounterFrequency(); + Require(frequency == sceKernelGetTscFrequency(), "process time counter frequency differs from the TSC frequency"); + const std::uint64_t counterMicros = counter / frequency * 1000000 + counter % frequency * 1000000 / frequency; + Require(counterMicros < minuteMicros, "first process time counter wrapped"); + Require(counterMicros >= minimumElapsedMicros, "first process time counter does not include the time before the first call"); + Require(sceKernelGetProcessTimeCounter() >= counter, "process time counter went backwards"); return 0; } const std::uint64_t first = sceKernelGetProcessTime(); 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/GuestPthreadAttrErrors.cpp b/core/libs/tests/GuestPthreadAttrErrors.cpp index 259ee7789c..06c2e15832 100644 --- a/core/libs/tests/GuestPthreadAttrErrors.cpp +++ b/core/libs/tests/GuestPthreadAttrErrors.cpp @@ -2,6 +2,7 @@ #include #include #include +#include extern "C" { int APS5_VABI scePthreadAttrInit(PthreadAttr* attr); @@ -11,7 +12,9 @@ int APS5_VABI scePthreadAttrGetstacksize(const PthreadAttr* attr, std::size_t* s int APS5_VABI scePthreadAttrSetdetachstate(PthreadAttr* attr, int state); int APS5_VABI scePthreadAttrGetdetachstate(const PthreadAttr* attr, int* state); int APS5_VABI scePthreadAttrSetschedpolicy(PthreadAttr* attr, int policy); +int APS5_VABI scePthreadAttrGetschedpolicy(const PthreadAttr* attr, int* policy); int APS5_VABI scePthreadAttrSetinheritsched(PthreadAttr* attr, int inheritSched); +int APS5_VABI scePthreadAttrGetinheritsched(const PthreadAttr* attr, int* inheritSched); int APS5_VABI scePthreadAttrSetschedparam(PthreadAttr* attr, const KernelSchedParam* param); int APS5_VABI scePthreadAttrGetschedparam(const PthreadAttr* attr, KernelSchedParam* param); int APS5_VABI scePthreadAttrSetaffinity(PthreadAttr* attr, KernelCpumask mask); @@ -27,6 +30,9 @@ static void Require(bool value) { if (!value) std::abort(); } int main() { PthreadAttr attr = nullptr; Require(scePthreadAttrInit(&attr) == SCE_OK); + int value = -1; + Require(scePthreadAttrGetschedpolicy(&attr, &value) == SCE_OK && value == 1); + Require(scePthreadAttrGetinheritsched(&attr, &value) == SCE_OK && value == 4); for (std::size_t size : {std::size_t{0}, std::size_t{1}, std::size_t{16383}}) Require(scePthreadAttrSetstacksize(&attr, size) == SCE_KERNEL_ERROR_EINVAL); @@ -35,11 +41,23 @@ int main() { for (int state : {-1, 2, 3, 100}) Require(scePthreadAttrSetdetachstate(&attr, state) == SCE_KERNEL_ERROR_EINVAL); for (int state : {0, 1}) Require(scePthreadAttrSetdetachstate(&attr, state) == SCE_OK); - for (int policy : {-1, 0, 2, 4}) Require(scePthreadAttrSetschedpolicy(&attr, policy) == SCE_KERNEL_ERROR_EOPNOTSUPP); - for (int policy : {1, 3}) Require(scePthreadAttrSetschedpolicy(&attr, policy) == SCE_OK); + for (int policy : {-1, 0, 2, 4}) { + Require(scePthreadAttrSetschedpolicy(&attr, policy) == SCE_KERNEL_ERROR_EOPNOTSUPP); + Require(scePthreadAttrGetschedpolicy(&attr, &value) == SCE_OK && value == 1); + } + for (int policy : {3, 1}) { + Require(scePthreadAttrSetschedpolicy(&attr, policy) == SCE_OK); + Require(scePthreadAttrGetschedpolicy(&attr, &value) == SCE_OK && value == policy); + } - for (int inherit : {-1, 1, 2, 8}) Require(scePthreadAttrSetinheritsched(&attr, inherit) == SCE_KERNEL_ERROR_EOPNOTSUPP); - for (int inherit : {0, 4}) Require(scePthreadAttrSetinheritsched(&attr, inherit) == SCE_OK); + for (int inherit : {-1, 1, 2, 8}) { + Require(scePthreadAttrSetinheritsched(&attr, inherit) == SCE_KERNEL_ERROR_EOPNOTSUPP); + Require(scePthreadAttrGetinheritsched(&attr, &value) == SCE_OK && value == 4); + } + for (int inherit : {0, 4}) { + Require(scePthreadAttrSetinheritsched(&attr, inherit) == SCE_OK); + Require(scePthreadAttrGetinheritsched(&attr, &value) == SCE_OK && value == inherit); + } KernelSchedParam param{}; for (int priority : {-1, 0, 255, 768, 900, 960}) { @@ -62,6 +80,10 @@ int main() { Require(scePthreadAttrGetstacksize(&attr, nullptr) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadAttrGetdetachstate(&attr, nullptr) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadAttrGetschedparam(&attr, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrGetschedpolicy(&attr, nullptr) == SCE_KERNEL_ERROR_EINVAL); + bool threw = false; + try { scePthreadAttrGetinheritsched(&attr, nullptr); } catch (const std::runtime_error&) { threw = true; } + Require(threw); for (PthreadAttr* missing : {static_cast(nullptr), static_cast(&attr)}) { PthreadAttr unset = nullptr; @@ -76,6 +98,8 @@ int main() { Require(scePthreadAttrGetstacksize(target, &size) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadAttrGetdetachstate(target, &state) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadAttrGetschedparam(target, ¶m) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrGetschedpolicy(target, &state) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrGetinheritsched(target, &state) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadAttrGetaffinity(target, &mask) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadAttrDestroy(target) == SCE_KERNEL_ERROR_EINVAL); } 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/GuestPthreadSchedparam.cpp b/core/libs/tests/GuestPthreadSchedparam.cpp index ddafd9090e..171d3fc5be 100644 --- a/core/libs/tests/GuestPthreadSchedparam.cpp +++ b/core/libs/tests/GuestPthreadSchedparam.cpp @@ -1,5 +1,7 @@ #include "SceTypes.hpp" #include +#include +#include #include extern "C" { @@ -9,9 +11,15 @@ Pthread APS5_VABI scePthreadSelf(void); int APS5_VABI scePthreadGetschedparam(Pthread thread, int* policy, KernelSchedParam* param); int APS5_VABI scePthreadSetschedparam(Pthread thread, int policy, const KernelSchedParam* param); int APS5_VABI pthread_getschedparam_nid_postfix(Pthread thread, int* policy, KernelSchedParam* param); +int APS5_VABI pthread_getprio_nid_postfix(Pthread thread); +int* APS5_VABI __error_nid_postfix(); int APS5_VABI pthread_setschedparam_nid_postfix(Pthread thread, int policy, const KernelSchedParam* param); int APS5_VABI scePthreadAttrInit(PthreadAttr* attr); int APS5_VABI scePthreadAttrDestroy(PthreadAttr* attr); +int APS5_VABI scePthreadAttrGet(Pthread thread, PthreadAttr* attr); +int APS5_VABI pthread_attr_get_np_nid_postfix(Pthread thread, PthreadAttr* attr); +int APS5_VABI scePthreadAttrGetschedpolicy(const PthreadAttr* attr, int* policy); +int APS5_VABI scePthreadAttrGetschedparam(const PthreadAttr* attr, KernelSchedParam* param); int APS5_VABI scePthreadAttrSetinheritsched(PthreadAttr* attr, int inheritSched); int APS5_VABI scePthreadAttrSetschedpolicy(PthreadAttr* attr, int policy); int APS5_VABI scePthreadAttrSetschedparam(PthreadAttr* attr, const KernelSchedParam* param); @@ -19,15 +27,37 @@ int APS5_VABI scePthreadAttrSetschedparam(PthreadAttr* attr, const KernelSchedPa static constexpr int SCE_OK = 0; static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016); +static constexpr int SCE_KERNEL_ERROR_ESRCH = static_cast(0x80020003); static constexpr int GUEST_EINVAL = 22; -static constexpr int SCHED_FIFO = 1; -static constexpr int SCHED_OTHER = 2; -static constexpr int SCHED_RR = 3; +static constexpr int GUEST_ESRCH = 3; +static constexpr int GUEST_SCHED_FIFO = 1; +static constexpr int GUEST_SCHED_OTHER = 2; +static constexpr int GUEST_SCHED_RR = 3; static constexpr int EXPLICIT_SCHED = 0; static constexpr int INHERIT_SCHED = 4; static void Require(bool value) { if (!value) std::abort(); } +static void RequireAttributes(Pthread thread, int policy, int priority) { + *__error_nid_postfix() = 1234; + Require(pthread_getprio_nid_postfix(thread) == priority); + Require(*__error_nid_postfix() == 1234); + PthreadAttr attr = nullptr; + Require(scePthreadAttrInit(&attr) == SCE_OK); + for (auto query : {scePthreadAttrGet, pthread_attr_get_np_nid_postfix}) { + for (int previous : {GUEST_SCHED_FIFO, GUEST_SCHED_RR}) { + Require(scePthreadAttrSetschedpolicy(&attr, previous) == SCE_OK); + Require(query(thread, &attr) == SCE_OK); + int current = -1; + KernelSchedParam param{}; + Require(scePthreadAttrGetschedpolicy(&attr, ¤t) == SCE_OK); + Require(scePthreadAttrGetschedparam(&attr, ¶m) == SCE_OK); + Require(current == policy && param.sched_priority == priority); + } + } + Require(scePthreadAttrDestroy(&attr) == SCE_OK); +} + static void RequireSched(int policy, int priority) { int current = -1; KernelSchedParam param{}; @@ -37,6 +67,7 @@ static void RequireSched(int policy, int priority) { param.sched_priority = -1; Require(pthread_getschedparam_nid_postfix(scePthreadSelf(), ¤t, ¶m) == 0); Require(current == policy && param.sched_priority == priority); + RequireAttributes(scePthreadSelf(), policy, priority); } static int SceSet(int policy, int priority) { @@ -53,36 +84,44 @@ static void* APS5_VABI Run(void*) { int policy = -1; KernelSchedParam param{}; Require(scePthreadGetschedparam(scePthreadSelf(), &policy, ¶m) == SCE_OK); - Require(policy == SCHED_FIFO); + Require(policy == GUEST_SCHED_FIFO); for (int priority : {256, 767, 896, 959}) { - Require(SceSet(SCHED_FIFO, priority) == SCE_OK); - RequireSched(SCHED_FIFO, priority); + Require(SceSet(GUEST_SCHED_FIFO, priority) == SCE_OK); + RequireSched(GUEST_SCHED_FIFO, priority); } for (int priority : {-1, 0, 255, 768, 895, 960, 1023, 4096}) { - Require(SceSet(SCHED_FIFO, priority) == SCE_KERNEL_ERROR_EINVAL); - Require(PosixSet(SCHED_FIFO, priority) == GUEST_EINVAL); - RequireSched(SCHED_FIFO, 959); + Require(SceSet(GUEST_SCHED_FIFO, priority) == SCE_KERNEL_ERROR_EINVAL); + Require(PosixSet(GUEST_SCHED_FIFO, priority) == GUEST_EINVAL); + RequireSched(GUEST_SCHED_FIFO, 959); } - Require(SceSet(SCHED_RR, 300) == SCE_OK); - RequireSched(SCHED_RR, 300); - for (int other : {-1, 0, SCHED_OTHER, 4}) { + Require(SceSet(GUEST_SCHED_RR, 300) == SCE_OK); + RequireSched(GUEST_SCHED_RR, 300); + for (int other : {-1, 0, GUEST_SCHED_OTHER, 4}) { Require(SceSet(other, 700) == SCE_KERNEL_ERROR_EINVAL); Require(PosixSet(other, 700) == GUEST_EINVAL); - RequireSched(SCHED_RR, 300); + RequireSched(GUEST_SCHED_RR, 300); } - Require(PosixSet(SCHED_FIFO, 700) == 0); - RequireSched(SCHED_FIFO, 700); - Require(scePthreadSetschedparam(scePthreadSelf(), SCHED_FIFO, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(PosixSet(GUEST_SCHED_FIFO, 700) == 0); + RequireSched(GUEST_SCHED_FIFO, 700); + Require(scePthreadSetschedparam(scePthreadSelf(), GUEST_SCHED_FIFO, nullptr) == SCE_KERNEL_ERROR_EINVAL); return nullptr; } +struct PolicyRequest { + std::promise ready; + std::future released; +}; + static void* APS5_VABI ReportPolicy(void* arg) { + auto& request = *static_cast(arg); int policy = -1; KernelSchedParam param{}; Require(scePthreadGetschedparam(scePthreadSelf(), &policy, ¶m) == SCE_OK); - *static_cast(arg) = policy; + RequireAttributes(scePthreadSelf(), policy, param.sched_priority); + request.ready.set_value(policy); + request.released.wait(); return nullptr; } @@ -91,10 +130,28 @@ static int CreatedPolicy(int inheritSched, int policy) { Require(scePthreadAttrInit(&attr) == SCE_OK); Require(scePthreadAttrSetinheritsched(&attr, inheritSched) == SCE_OK); Require(scePthreadAttrSetschedpolicy(&attr, policy) == SCE_OK); - int reported = -1; + std::promise release; + PolicyRequest request{{}, release.get_future()}; + auto ready = request.ready.get_future(); Pthread thread = nullptr; - Require(scePthreadCreate(&thread, &attr, ReportPolicy, &reported, "policy") == SCE_OK); + Require(scePthreadCreate(&thread, &attr, ReportPolicy, &request, "policy") == SCE_OK); + const int reported = ready.get(); + int current = -1; + KernelSchedParam param{}; + Require(scePthreadGetschedparam(thread, ¤t, ¶m) == SCE_OK); + Require(current == reported); + RequireAttributes(thread, reported, param.sched_priority); + for (int updated : {GUEST_SCHED_RR, GUEST_SCHED_FIFO}) { + const KernelSchedParam requested{450}; + Require(scePthreadSetschedparam(thread, updated, &requested) == SCE_OK); + RequireAttributes(thread, updated, requested.sched_priority); + } + release.set_value(); Require(scePthreadJoin(thread, nullptr) == SCE_OK); + Require(scePthreadAttrSetschedpolicy(&attr, GUEST_SCHED_RR) == SCE_OK); + Require(scePthreadAttrGet(thread, &attr) == SCE_KERNEL_ERROR_ESRCH); + current = -1; + Require(scePthreadAttrGetschedpolicy(&attr, ¤t) == SCE_OK && current == GUEST_SCHED_RR); Require(scePthreadAttrDestroy(&attr) == SCE_OK); return reported; } @@ -119,7 +176,7 @@ static int CreatedPriority(int inheritSched, const KernelSchedParam* param) { PthreadAttr attr = nullptr; Require(scePthreadAttrInit(&attr) == SCE_OK); Require(scePthreadAttrSetinheritsched(&attr, inheritSched) == SCE_OK); - Require(scePthreadAttrSetschedpolicy(&attr, SCHED_RR) == SCE_OK); + Require(scePthreadAttrSetschedpolicy(&attr, GUEST_SCHED_RR) == SCE_OK); if (param) Require(scePthreadAttrSetschedparam(&attr, param) == SCE_OK); const int reported = CreatedPriority(&attr); Require(scePthreadAttrDestroy(&attr) == SCE_OK); @@ -129,23 +186,31 @@ static int CreatedPriority(int inheritSched, const KernelSchedParam* param) { static void* APS5_VABI Spawn(void*) { const KernelSchedParam param{300}; for (int priority : {450, 600}) { - Require(SceSet(SCHED_FIFO, priority) == SCE_OK); + Require(SceSet(GUEST_SCHED_FIFO, priority) == SCE_OK); Require(CreatedPriority(INHERIT_SCHED, ¶m) == priority); Require(CreatedPriority(INHERIT_SCHED, nullptr) == priority); Require(CreatedPriority(nullptr) == priority); Require(CreatedPriority(EXPLICIT_SCHED, ¶m) == 300); Require(CreatedPriority(EXPLICIT_SCHED, nullptr) == 700); } + Require(SceSet(GUEST_SCHED_RR, 300) == SCE_OK); + Require(CreatedPolicy(INHERIT_SCHED, GUEST_SCHED_FIFO) == GUEST_SCHED_RR); return nullptr; } int main() { + *__error_nid_postfix() = 1234; + Require(pthread_getprio_nid_postfix(nullptr) == -1 && *__error_nid_postfix() == GUEST_ESRCH); + Require(pthread_getprio_nid_postfix(reinterpret_cast(std::uintptr_t{1})) == -1 && *__error_nid_postfix() == GUEST_ESRCH); Pthread thread = nullptr; Require(scePthreadCreate(&thread, nullptr, Run, nullptr, "schedparam") == SCE_OK); + const Pthread retired = thread; Require(scePthreadJoin(thread, nullptr) == SCE_OK); - Require(CreatedPolicy(EXPLICIT_SCHED, SCHED_RR) == SCHED_RR); - Require(CreatedPolicy(EXPLICIT_SCHED, SCHED_FIFO) == SCHED_FIFO); - Require(CreatedPolicy(INHERIT_SCHED, SCHED_RR) == SCHED_FIFO); + *__error_nid_postfix() = 1234; + Require(pthread_getprio_nid_postfix(retired) == -1 && *__error_nid_postfix() == GUEST_ESRCH); + Require(CreatedPolicy(EXPLICIT_SCHED, GUEST_SCHED_RR) == GUEST_SCHED_RR); + Require(CreatedPolicy(EXPLICIT_SCHED, GUEST_SCHED_FIFO) == GUEST_SCHED_FIFO); + Require(CreatedPolicy(INHERIT_SCHED, GUEST_SCHED_RR) == GUEST_SCHED_FIFO); Require(scePthreadCreate(&thread, nullptr, Spawn, nullptr, "spawn") == SCE_OK); Require(scePthreadJoin(thread, nullptr) == SCE_OK); } 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/GuestRaiseInSystemCall.cpp b/core/libs/tests/GuestRaiseInSystemCall.cpp new file mode 100644 index 0000000000..834594d260 --- /dev/null +++ b/core/libs/tests/GuestRaiseInSystemCall.cpp @@ -0,0 +1,86 @@ +#include "SceTypes.hpp" +#include +#include +#include +#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 = 10; +static constexpr DWORD PipeBytes = 4096; +static void Require(bool value) { if (!value) std::abort(); } + +static std::FILE* shared = nullptr; +static std::atomic deliveries{0}; +static std::atomic released{0}; +static std::atomic stop{false}; +static std::atomic blocks{0}; + +static 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(16u * PipeBytes, 'x'); + while (!stop.load()) { + Require(std::fwrite(block.data(), 1, block.size(), shared) == block.size()); + blocks.fetch_add(1); + volatile std::uint64_t spin = 0; + for (std::uint64_t i = 0; i < 200000; ++i) spin = spin + i; + } + return nullptr; +} + +int main() { + std::atomic done{false}; + std::thread watchdog([&done] { + for (int i = 0; i < 25000 && !done.load(); ++i) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + if (!done.load()) std::abort(); + }); + HANDLE read = nullptr; + HANDLE write = nullptr; + Require(CreatePipe(&read, &write, nullptr, PipeBytes) != 0); + shared = _fdopen(_open_osfhandle(reinterpret_cast(write), _O_WRONLY | _O_BINARY), "wb"); + Require(shared != nullptr); + std::atomic draining{true}; + std::thread reader([&] { + char chunk[PipeBytes]; + DWORD got = 0; + while (ReadFile(read, chunk, sizeof(chunk), &got, nullptr) != 0 && got != 0) { + if (draining.load()) std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + }); + Require(sceKernelInstallExceptionHandler(SigUsr1, reinterpret_cast(&Handler)) == 0); + Pthread writer = nullptr; + Require(scePthreadCreate(&writer, nullptr, Writer, nullptr, "pipe writer") == 0); + while (blocks.load() == 0) std::this_thread::yield(); + for (int raise = 1; raise <= Raises; ++raise) { + Require(sceKernelRaiseException(writer, SigUsr1) == 0); + while (deliveries.load() < raise) std::this_thread::yield(); + Require(std::fputc('y', shared) == 'y'); + released.store(raise); + } + stop.store(true); + draining.store(false); + Require(scePthreadJoin(writer, nullptr) == 0); + Require(sceKernelRemoveExceptionHandler(SigUsr1) == 0); + std::fclose(shared); + reader.join(); + CloseHandle(read); + done.store(true); + watchdog.join(); +} diff --git a/core/libs/tests/GuestRandomState.cpp b/core/libs/tests/GuestRandomState.cpp index 6f2ab74c7d..71511241be 100644 --- a/core/libs/tests/GuestRandomState.cpp +++ b/core/libs/tests/GuestRandomState.cpp @@ -61,7 +61,7 @@ static void CheckRand48() { Require(jrand48_nid_postfix(seed) == 1517566982 && SeedIs(seed, 0x2a23, 0x3c06, 0x5a74), "jrand48 third value"); Require(erand48_nid_postfix(seed) == 0x72534abf62f2p-48 && SeedIs(seed, 0x62f2, 0x4abf, 0x7253), "erand48 fourth value"); Require(nrand48_nid_postfix(seed) == 684387517 && SeedIs(seed, 0x8d15, 0xd97a, 0x5195), "nrand48 fifth value"); - Require(jrand48_nid_postfix(seed) == -487786166 && SeedIs(seed, 0xeffc, 0xf94a, 0xe2ec), "jrand48 negative value"); + Require(jrand48_nid_postfix(seed) == 3807181130 && SeedIs(seed, 0xeffc, 0xf94a, 0xe2ec), "jrand48 keeps bit 31 without sign extension"); std::uint16_t zero[3] = {}; Require(nrand48_nid_postfix(zero) == 0 && SeedIs(zero, 0x000b, 0, 0), "rand48 zero seed adds the increment"); @@ -93,20 +93,30 @@ static void CheckRand48() { std::uint16_t reset[3] = {0x330e, 0xabcd, 0x1234}; Require(lrand48_nid_postfix() == nrand48_nid_postfix(reset), "srand48 restores the default multiplier and addend"); + std::uint16_t measured[3] = {0x000b, 0x0016, 0x0021}; + Require(jrand48_nid_postfix(measured) == 3702088075 && SeedIs(measured, 0xe6ba, 0x618b, 0xdca9), "jrand48 matches the console for a result with bit 31 set"); + std::uint16_t topBit[3] = {0, 0, 0x8000}; + Require(jrand48_nid_postfix(topBit) == 2147483648, "jrand48 matches the console for 0x80000000"); + std::uint16_t ones[3] = {0xffff, 0xffff, 0xffff}; + Require(jrand48_nid_postfix(ones) == 4294582547, "jrand48 matches the console for an all-ones seed"); + constexpr std::array consoleMrand48 {178800969, 1952030186, 3585512650, 1443049011, 2428758494, 7588830}; + srand48_nid_postfix(1); + for (const std::int64_t expected : consoleMrand48) Require(mrand48_nid_postfix() == expected, "mrand48 matches the console after srand48(1)"); + #ifndef _WIN32 std::uint16_t guest[3] = {0x1234, 0x5678, 0x9abc}; unsigned short host[3] = {0x1234, 0x5678, 0x9abc}; for (int round = 0; round < 64; ++round) { Require(erand48_nid_postfix(guest) == ::erand48(host), "erand48 matches the host"); Require(nrand48_nid_postfix(guest) == ::nrand48(host), "nrand48 matches the host"); - Require(jrand48_nid_postfix(guest) == ::jrand48(host), "jrand48 matches the host"); + Require(jrand48_nid_postfix(guest) == static_cast(::jrand48(host)), "jrand48 matches the host's 32 bits"); } ::srand48(1234567); srand48_nid_postfix(1234567); for (int round = 0; round < 32; ++round) { Require(drand48_nid_postfix() == ::drand48(), "drand48 matches the host"); Require(lrand48_nid_postfix() == ::lrand48(), "lrand48 matches the host"); - Require(mrand48_nid_postfix() == ::mrand48(), "mrand48 matches the host"); + Require(mrand48_nid_postfix() == static_cast(::mrand48()), "mrand48 matches the host's 32 bits"); } #endif } diff --git a/core/libs/tests/GuestRazorCpu.cpp b/core/libs/tests/GuestRazorCpu.cpp index 19ac107f19..13572d5feb 100644 --- a/core/libs/tests/GuestRazorCpu.cpp +++ b/core/libs/tests/GuestRazorCpu.cpp @@ -1,6 +1,9 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include +#include +#include extern "C" { std::uint32_t APS5_VABI sceRazorCpuIsCapturing(void); @@ -15,6 +18,16 @@ int APS5_VABI sceRazorCpuFlushOccurred(std::uint64_t* timeSpentInFlush); int APS5_VABI sceRazorCpuPlotValue(const char* series, float value); int APS5_VABI sceRazorCpuWriteBookmark(const char* label, const char* description); int APS5_VABI sceRazorCpuSync(void); +void APS5_VABI sceRazorCpuDisableFiberUserMarkers(void); +int APS5_VABI sceRazorCpuInitDataTags(void* buffer, std::size_t size); +int APS5_VABI sceRazorCpuShutdownDataTags(void); +std::size_t APS5_VABI sceRazorCpuGetDataTagStorageSize(std::uint32_t tagCount); +int APS5_VABI sceRazorCpuTagArray(const char* label, std::uint32_t category, void* array, std::size_t bufferSize, std::size_t elementSize); +int APS5_VABI sceRazorCpuTagBuffer(const char* label, std::uint32_t category, void* buffer, std::size_t bufferSize); +int APS5_VABI sceRazorCpuResizeTaggedBuffer(std::uint32_t category, void* buffer, std::size_t newBufferSize); +int APS5_VABI sceRazorCpuUnTagBuffer(std::uint32_t category, void* buffer); +int APS5_VABI sceRazorCpuBeginLogicalFileAccess(const char* filename, std::uint64_t tag, std::uint64_t size, std::uint8_t operation); +int APS5_VABI sceRazorCpuEndLogicalFileAccess(void); } namespace { @@ -47,4 +60,39 @@ int main() { Require(sceRazorCpuSync() == 0); Require(sceRazorCpuSync() == 0); Require(sceRazorCpuIsCapturing() == 0); + + sceRazorCpuDisableFiberUserMarkers(); + sceRazorCpuDisableFiberUserMarkers(); + Require(sceRazorCpuPushMarker("thread", 0x80ffffffu, 0) == 0); + Require(sceRazorCpuPopMarker() == 0); + + Require(sceRazorCpuBeginLogicalFileAccess("data/level.pak", 0x0123456789abcdefull, 4096, 0) == 0); + Require(sceRazorCpuEndLogicalFileAccess() == 0); + Require(sceRazorCpuBeginLogicalFileAccess("save/slot0.bin", 0, 512, 1) == 0); + Require(sceRazorCpuEndLogicalFileAccess() == 0); + Require(sceRazorCpuEndLogicalFileAccess() == 0); + + unsigned char storage[256]; + std::memset(storage, 0x5a, sizeof(storage)); + int elements[16]{}; + unsigned char buffer[64]{}; + Require(sceRazorCpuInitDataTags(storage, sizeof(storage)) == 0); + Require(sceRazorCpuTagArray("elements", 7, elements, sizeof(elements), sizeof(elements[0])) == 0); + Require(sceRazorCpuTagBuffer("buffer", 7, buffer, sizeof(buffer)) == 0); + Require(sceRazorCpuResizeTaggedBuffer(7, buffer, sizeof(buffer) / 2) == 0); + Require(sceRazorCpuUnTagBuffer(7, buffer) == 0); + Require(sceRazorCpuUnTagBuffer(7, elements) == 0); + Require(sceRazorCpuShutdownDataTags() == 0); + for (const unsigned char byte : storage) Require(byte == 0x5a); + Require(sceRazorCpuTagBuffer("untracked", 3, buffer, sizeof(buffer)) == 0); + Require(sceRazorCpuUnTagBuffer(3, buffer) == 0); + Require(sceRazorCpuIsCapturing() == 0); + + bool threw = false; + try { + sceRazorCpuGetDataTagStorageSize(16); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); } diff --git a/core/libs/tests/GuestRenameDot.cpp b/core/libs/tests/GuestRenameDot.cpp new file mode 100644 index 0000000000..ed44b81e35 --- /dev/null +++ b/core/libs/tests/GuestRenameDot.cpp @@ -0,0 +1,104 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#ifndef _WIN32 +#include +#endif + +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 ErrorNotDirectory = 20; +constexpr int ErrorInvalid = 22; +constexpr int ErrorLoop = 62; +constexpr char Source[] = "anyps5_rename_dot_source"; +constexpr char Target[] = "anyps5_rename_dot_target"; +constexpr char Moved[] = "anyps5_rename_dot_moved"; +constexpr char PlainFile[] = "anyps5_rename_dot_file"; +constexpr char Loop[] = "anyps5_rename_dot_loop"; + +void Cleanup() { + rmdir_nid_postfix(Source); + rmdir_nid_postfix(Target); + rmdir_nid_postfix(Moved); + std::remove(PlainFile); + std::remove(Loop); +} + +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"); + + { + std::FILE* file = std::fopen(PlainFile, "w"); + Require(file != nullptr && std::fclose(file) == 0, "creating the plain file failed"); + } + const std::string throughFile = std::string(PlainFile) + "/."; + Require(Fails(rename_nid_postfix(throughFile.c_str(), Moved), ErrorNotDirectory), "rename from file/. must fail with ENOTDIR (20)"); + Require(Fails(rename_nid_postfix(Source, throughFile.c_str()), ErrorNotDirectory), "rename to file/. must fail with ENOTDIR (20)"); + std::remove(PlainFile); +#ifndef _WIN32 + Require(::symlink(Loop, Loop) == 0, "creating the symbolic link loop failed"); + const std::string throughLoop = std::string(Loop) + "/."; + Require(Fails(rename_nid_postfix(throughLoop.c_str(), Moved), ErrorLoop), "rename from loop/. must fail with ELOOP (62)"); + std::remove(Loop); +#endif + + 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/GuestRenderingWait.cpp b/core/libs/tests/GuestRenderingWait.cpp index c598c5de35..a17c039713 100644 --- a/core/libs/tests/GuestRenderingWait.cpp +++ b/core/libs/tests/GuestRenderingWait.cpp @@ -10,6 +10,11 @@ #include extern "C" std::uint32_t APS5_VABI sceAgcDriverGetWaitRenderingPacketSizeInDwords(); extern "C" std::uint32_t APS5_VABI sceAgcDriverGetSetFlipPacketSizeInDwords(); +extern "C" std::uint32_t APS5_VABI sceAgcDriverGetSetWorkloadsActivePacketSize(); +extern "C" std::uint32_t APS5_VABI sceAgcDriverGetSetWorkloadCompletePacketSize(); +extern "C" std::uint32_t APS5_VABI sceAgcDriverGetSetWorkloadStreamInactivePacketSize(); +extern "C" std::uint32_t APS5_VABI sceAgcDriverNopDataGetPacketSize(std::uint32_t); +extern "C" std::uint32_t APS5_VABI sceAgcDriverUserDataGetPacketSize(std::uint32_t); extern "C" std::uint32_t APS5_VABI sceAgcDriverWaitUntilSafeForRendering(std::uint32_t**, std::uint32_t, std::uint32_t, std::uint32_t, int); static void Require(bool value) { if (!value) std::abort(); } class Output final : public AgcDriver::IVideoOutput, public AgcDriver::IRenderingWait, @@ -58,6 +63,13 @@ int main() { const auto flipPacketSize = sceAgcDriverGetSetFlipPacketSizeInDwords(); Require(flipPacketSize == AgcDriver::FlipPacketWords); Require(flipPacketSize == 6); + Require(sceAgcDriverGetSetWorkloadsActivePacketSize() == 18); + Require(sceAgcDriverGetSetWorkloadCompletePacketSize() == 12); + Require(sceAgcDriverGetSetWorkloadStreamInactivePacketSize() == 9); + Require(sceAgcDriverNopDataGetPacketSize(0) == 2 && sceAgcDriverNopDataGetPacketSize(1) == 3 && sceAgcDriverNopDataGetPacketSize(4) == 3); + Require(sceAgcDriverNopDataGetPacketSize(5) == 4 && sceAgcDriverNopDataGetPacketSize(0xffffffffu) == 0x40000002u); + Require(sceAgcDriverUserDataGetPacketSize(0) == 3 && sceAgcDriverUserDataGetPacketSize(1) == 4 && sceAgcDriverUserDataGetPacketSize(4) == 4); + Require(sceAgcDriverUserDataGetPacketSize(5) == 9 && sceAgcDriverUserDataGetPacketSize(0xffffffffu) == 0x40000007u); bool rejected = false; try { sceAgcDriverWaitUntilSafeForRendering(&cursor, size - 1, 0, 7, 0); } catch (const std::invalid_argument&) { rejected = true; } diff --git a/core/libs/tests/GuestRtc.cpp b/core/libs/tests/GuestRtc.cpp index a1d820e72d..a5585f843d 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); @@ -240,7 +271,14 @@ int main() { Require(std::memcmp(text, original, sizeof(text)) == 0); } Require(sceRtcFormatRFC3339(nullptr, &tick, 1440) == invalidPointer); - Require(sceRtcFormatRFC3339(text, nullptr, 1440) == invalidPointer); + Require(sceRtcFormatRFC3339(text, nullptr, 1440) == invalidValue); + { + RtcTick before{}, after{}, parsed{}; + Require(sceRtcGetCurrentTick(&before) == 0); + Require(sceRtcFormatRFC3339(text, nullptr, 0) == 0); + Require(sceRtcGetCurrentTick(&after) == 0); + Require(sceRtcParseRFC3339(&parsed, text) == 0 && parsed.tick + 10000 > before.tick && parsed.tick <= after.tick); + } Require(sceRtcParseRFC3339(&tick, "2024-02-29T14:04:56.789+01:30") == 0 && tick.tick == leapDayTick); Require(sceRtcParseRFC3339(&tick, "2024-02-29t12:34:56.789z") == 0 && tick.tick == leapDayTick); Require(sceRtcParseRFC3339(&tick, "1970-01-01T00:00:00Z") == 0 && tick.tick == unixEpochTick); @@ -335,6 +373,11 @@ int main() { {"Thu, 01 Mar 2024 12:34:56 nzdt", 0, march, 780}, {"Thu, 01 Mar 2024 12:34:56 KST", 0, march, 540}, {"Thu, 01 Mar 2024 12:34:56 HST", 0, march, 420}, + {"Thu, 01 Mar 2024 12:34:56 DNT", 0, march, 60}, + {"Thu, 01 Mar 2024 12:34:56 NFT", 0, march, -150}, + {"Thu, 01 Mar 2024 12:34:56 NOR", 0, march, 60}, + {"Thu, 01 Mar 2024 12:34:56 SET", 0, march, 60}, + {"Thu, 01 Mar 2024 12:34:56 SETX", 0, march, 60}, {"Thu, 01 Mar 2024 12:34:56 Jt", 0, march, 450}, {"Thu, 01 Mar 2024 12:34:56 ut", 0, march, -420}, {"Thu, 01 Mar 2024 12:34:56 xT", 0, march, 0}, @@ -431,12 +474,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 +486,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 +495,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); @@ -525,7 +769,15 @@ int main() { Require(std::memcmp(rfc2822, original, sizeof(rfc2822)) == 0); } Require(sceRtcFormatRFC2822(nullptr, &tick, 0) == invalidPointer); - Require(sceRtcFormatRFC2822(rfc2822, nullptr, 0) == invalidPointer); + { + RtcTick before{}, after{}, parsed{}; + Require(sceRtcGetCurrentTick(&before) == 0); + Require(sceRtcFormatRFC2822(rfc2822, nullptr, 60) == 0); + Require(sceRtcGetCurrentTick(&after) == 0); + Require(std::strlen(rfc2822) == 31 && std::strcmp(rfc2822 + 26, "+0100") == 0); + Require(sceRtcParseDateTime(&parsed, rfc2822) == 0 && parsed.tick + 1000000 > before.tick && parsed.tick <= after.tick); + Require(sceRtcFormatRFC2822(rfc2822, nullptr, 1440) == invalidValue); + } SetTimeZone("XXX-5:30"); Require(sceRtcFormatRFC2822LocalTime(rfc2822, &tick) == 0 && std::strcmp(rfc2822, "Thu, 29 Feb 2024 18:04:56 +0530") == 0); @@ -537,9 +789,16 @@ int main() { Require(sceRtcFormatRFC2822LocalTime(rfc2822, &tick) == 0 && std::strcmp(rfc2822, "Thu, 29 Feb 2024 12:34:56 +0000") == 0); Require(sceRtcFormatRFC3339LocalTime(text, &tick) == 0 && std::strcmp(text, "2024-02-29T12:34:56.78Z") == 0); Require(sceRtcFormatRFC2822LocalTime(nullptr, &tick) == invalidPointer); - Require(sceRtcFormatRFC2822LocalTime(rfc2822, nullptr) == invalidPointer); Require(sceRtcFormatRFC3339LocalTime(nullptr, &tick) == invalidPointer); - Require(sceRtcFormatRFC3339LocalTime(text, nullptr) == invalidPointer); + { + RtcTick before{}, after{}, parsed{}; + Require(sceRtcGetCurrentTick(&before) == 0); + Require(sceRtcFormatRFC2822LocalTime(rfc2822, nullptr) == 0); + Require(sceRtcFormatRFC3339LocalTime(text, nullptr) == 0); + Require(sceRtcGetCurrentTick(&after) == 0); + Require(sceRtcParseDateTime(&parsed, rfc2822) == 0 && parsed.tick + 1000000 > before.tick && parsed.tick <= after.tick); + Require(sceRtcParseRFC3339(&parsed, text) == 0 && parsed.tick + 10000 > before.tick && parsed.tick <= after.tick); + } for (const char* zone : {"UTC0", "XXX-5:30", "XXX+3"}) { SetTimeZone(zone); for (std::uint64_t invalidTick : {maxTick + 1, std::uint64_t{1} << 63u, std::numeric_limits::max()}) { @@ -613,5 +872,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/GuestRudpPump.cpp b/core/libs/tests/GuestRudpPump.cpp index 1e8ec7ab29..d9e350ae23 100644 --- a/core/libs/tests/GuestRudpPump.cpp +++ b/core/libs/tests/GuestRudpPump.cpp @@ -3,7 +3,6 @@ #include #include #include -#include #include extern "C" { @@ -13,6 +12,7 @@ int APS5_VABI sceRudpProcessEvents(); int APS5_VABI sceRudpNetFlush(); int APS5_VABI sceRudpGetMaxSegmentSize(std::uint16_t*); int APS5_VABI sceRudpNetReceived(); +int APS5_VABI sceRudpEnableInternalIOThread2(std::uint32_t, std::uint32_t, std::uint64_t); } static void Require(bool value, int line) { @@ -29,6 +29,8 @@ int main() { std::uint16_t segmentSize = 0x5a5a; CHECK(sceRudpProcessEvents() == notInitialized); CHECK(sceRudpNetFlush() == notInitialized); + CHECK(sceRudpNetReceived() == notInitialized); + CHECK(sceRudpEnableInternalIOThread2(0, 0, 0) == notInitialized); CHECK(sceRudpGetMaxSegmentSize(&segmentSize) == notInitialized); CHECK(segmentSize == 0x5a5a); CHECK(sceRudpGetMaxSegmentSize(nullptr) == notInitialized); @@ -37,6 +39,8 @@ int main() { CHECK(sceRudpInit_nid_postfix(pool, sizeof(pool)) == 0); CHECK(sceRudpProcessEvents() == 0); CHECK(sceRudpNetFlush() == 0); + CHECK(sceRudpNetReceived() == 0); + CHECK(sceRudpEnableInternalIOThread2(0x4000, 0, 0x3) == 0); CHECK(sceRudpGetMaxSegmentSize(nullptr) == invalidArgument); CHECK(sceRudpGetMaxSegmentSize(&segmentSize) == 0); CHECK(segmentSize == 1410); @@ -49,12 +53,6 @@ int main() { CHECK(sceRudpGetMaxSegmentSize(&segmentSize) == notInitialized); CHECK(segmentSize == 0x5a5a); - bool threw = false; - try { - sceRudpNetReceived(); - } catch (const std::runtime_error&) { - threw = true; - } - CHECK(threw); + CHECK(sceRudpNetReceived() == notInitialized); return 0; } diff --git a/core/libs/tests/GuestRuneLocale.cpp b/core/libs/tests/GuestRuneLocale.cpp index 86d5de99b0..e677153ff7 100644 --- a/core/libs/tests/GuestRuneLocale.cpp +++ b/core/libs/tests/GuestRuneLocale.cpp @@ -1,4 +1,6 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include +#include #include #include #include @@ -38,6 +40,12 @@ int APS5_VABI iswctype_nid_postfix(std::uint32_t, std::uint64_t); std::uint64_t APS5_VABI wctype_nid_postfix(const char*); std::uint32_t APS5_VABI towlower_nid_postfix(std::uint32_t); std::uint32_t APS5_VABI towupper_nid_postfix(std::uint32_t); +std::uint64_t APS5_VABI wctrans_nid_postfix(const char*); +std::uint32_t APS5_VABI towctrans_nid_postfix(std::uint32_t, std::uint64_t); +std::uint32_t APS5_VABI _Towlower_nid_postfix(std::uint32_t, const void*); +std::uint32_t APS5_VABI _Towupper_nid_postfix(std::uint32_t, const void*); +int APS5_VABI _Tolower_nid_postfix(int, const void*); +int APS5_VABI _Toupper_nid_postfix(int, const void*); std::uint32_t APS5_VABI btowc_nid_postfix(int); int APS5_VABI wctob_nid_postfix(std::uint32_t); } @@ -125,5 +133,19 @@ int main() { Require(btowc_nid_postfix('A') == 'A' && btowc_nid_postfix(0xe9) == 0xe9 && btowc_nid_postfix(-23) == 0xe9); Require(wctob_nid_postfix('A') == 'A' && wctob_nid_postfix(0xe9) == 0xe9 && wctob_nid_postfix(0) == 0); Require(wctob_nid_postfix(0x100) == -1 && wctob_nid_postfix(0x20ac) == -1 && wctob_nid_postfix(0xffffffff) == -1); + Require(wctrans_nid_postfix("tolower") == 1 && wctrans_nid_postfix("toupper") == 2); + Require(wctrans_nid_postfix("") == 0 && wctrans_nid_postfix("TOLOWER") == 0 && wctrans_nid_postfix("tolowe") == 0); + for (std::int64_t c = -300; c < 70000; ++c) { + const auto wide = static_cast(c); + const bool ascii = c >= 0 && c < 128; + const auto lower = ascii ? static_cast(std::tolower(static_cast(c))) : wide; + const auto upper = ascii ? static_cast(std::toupper(static_cast(c))) : wide; + Require(towctrans_nid_postfix(wide, 1) == lower && towctrans_nid_postfix(wide, 2) == upper); + Require(_Towlower_nid_postfix(wide, nullptr) == lower && _Towupper_nid_postfix(wide, nullptr) == upper); + Require(_Tolower_nid_postfix(static_cast(c), nullptr) == static_cast(lower)); + Require(_Toupper_nid_postfix(static_cast(c), nullptr) == static_cast(upper)); + } + errno = 0; + Require(towctrans_nid_postfix('A', 0) == 'A' && towctrans_nid_postfix('a', 3) == 'a' && errno == 0); return 0; } diff --git a/core/libs/tests/GuestSandboxCertificates.cpp b/core/libs/tests/GuestSandboxCertificates.cpp index c95eded2f8..2c76c900ba 100644 --- a/core/libs/tests/GuestSandboxCertificates.cpp +++ b/core/libs/tests/GuestSandboxCertificates.cpp @@ -73,8 +73,13 @@ static void CheckOverrides(const std::string& directory, const std::string& path int main() { #ifndef _WIN32 const auto bundle = std::filesystem::absolute("host_bundle.pem"); - const std::string expected = "-----BEGIN CERTIFICATE-----\nMIIBAA==\n-----END CERTIFICATE-----\n"; - { std::ofstream file(bundle, std::ios::binary | std::ios::trunc); file << expected; } + const std::string expected = "-----BEGIN CERTIFICATE-----\nMIIBAA==\n-----END CERTIFICATE-----\n" + "-----BEGIN CERTIFICATE-----\r\nMIIBAg==\r\n-----END CERTIFICATE-----\r\n"; + const std::string input = "Bag Attributes\r\n friendlyName: should-not-be-copied\r\n" + "-----BEGIN PRIVATE KEY-----\r\nprivate-key-material\r\n-----END PRIVATE KEY-----\r\n" + + expected + "subject=should-not-be-copied\r\n" + "-----BEGIN RSA PRIVATE KEY-----\r\nmore-private-key-material\r\n-----END RSA PRIVATE KEY-----\r\n"; + { std::ofstream file(bundle, std::ios::binary | std::ios::trunc); file << input; } Require(setenv("SSL_CERT_FILE", bundle.c_str(), 1) == 0); #endif const std::string directory = std::string("/") + sceKernelGetFsSandboxRandomWord() + "/common/cert"; diff --git a/core/libs/tests/GuestSaveDataDialog.cpp b/core/libs/tests/GuestSaveDataDialog.cpp index 25b68bdcb5..1c3ffc702d 100644 --- a/core/libs/tests/GuestSaveDataDialog.cpp +++ b/core/libs/tests/GuestSaveDataDialog.cpp @@ -34,8 +34,12 @@ static void RequireRunning() { } int main() { + SaveDataDialogParam param{}; + param.size = sizeof(param); + Require(sceSaveDataDialogGetStatus() == SAVE_DATA_DIALOG_STATUS_NONE, "initial status"); Require(sceSaveDataDialogTerminate() == SAVE_DATA_DIALOG_ERROR_NOT_INITIALIZED, "terminate before init"); + Require(sceSaveDataDialogOpen(¶m) == SAVE_DATA_DIALOG_ERROR_INVALID_STATE, "open before init"); Require(sceSaveDataDialogClose(nullptr) == SAVE_DATA_DIALOG_ERROR_NOT_RUNNING, "close before init"); SaveDataDialogResult uninitResult{}; Require(sceSaveDataDialogGetResult(&uninitResult) == SAVE_DATA_DIALOG_ERROR_NOT_FINISHED, "get result before init"); @@ -44,6 +48,7 @@ int main() { Require(sceSaveDataDialogInitialize() == 0, "initialize"); Require(sceSaveDataDialogInitialize() == SAVE_DATA_DIALOG_ERROR_ALREADY_INITIALIZED, "double init"); + Require(sceSaveDataDialogOpen(nullptr) == SAVE_DATA_DIALOG_ERROR_ARG_NULL, "open with null param"); Require(sceSaveDataDialogClose(nullptr) == SAVE_DATA_DIALOG_ERROR_NOT_RUNNING, "close while initialized not running"); Require(sceSaveDataDialogGetResult(&uninitResult) == SAVE_DATA_DIALOG_ERROR_NOT_FINISHED, "get result before open"); Require(sceSaveDataDialogProgressBarInc(0, 10) == SAVE_DATA_DIALOG_ERROR_NOT_RUNNING, "progress inc while initialized not running"); @@ -54,7 +59,7 @@ int main() { items.dir_names = names; items.dir_names_num = 2; int context = 42; - SaveDataDialogParam param{}; + param = {}; param.size = sizeof(param); param.mode = SAVE_DATA_DIALOG_MODE_PROGRESS_BAR; param.items = &items; @@ -80,6 +85,8 @@ int main() { SaveDataDialogResult result{}; result.dir_name = &selected; Require(sceSaveDataDialogGetResult(&result) == 0, "read completed result"); + Require(sceSaveDataDialogGetResult(nullptr) == SAVE_DATA_DIALOG_ERROR_ARG_NULL, + "get result with null result pointer"); Require(result.mode == param.mode && result.result == SAVE_DATA_DIALOG_RESULT_OK && result.user_data == &context && std::strcmp(selected.data, "SDU1") == 0, "save result must preserve its mode, directory and caller context"); diff --git a/core/libs/tests/GuestSaveDataMountStatus.cpp b/core/libs/tests/GuestSaveDataMountStatus.cpp index 92571b6b8c..88557e574d 100644 --- a/core/libs/tests/GuestSaveDataMountStatus.cpp +++ b/core/libs/tests/GuestSaveDataMountStatus.cpp @@ -14,6 +14,10 @@ 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); +int APS5_VABI sceSaveDataLoadIcon(const SaveDataMountPoint*, SaveDataIcon*); +int APS5_VABI sceSaveDataSaveIcon(const SaveDataMountPoint*, const SaveDataIcon*); +int APS5_VABI sceSaveDataSaveIconByPath(const SaveDataMountPoint*, const char*); } namespace { @@ -38,23 +42,43 @@ void CheckUnmountedCalls() { SaveDataMountPoint point{}; std::memcpy(point.data, "test", sizeof("test")); SaveDataMountInfo info{}; + SaveDataIcon icon{}; char buf[128]{}; std::size_t gotSize = 0; + Check(sceSaveDataUmount2(0, nullptr) == SaveDataErrorParameter, "umount2 null point is PARAMETER"); + Check(sceSaveDataUmount2(0, &point) == SaveDataErrorNotMounted, "umount2 unmounted point is NOT_MOUNTED"); + Check(sceSaveDataLoadIcon(nullptr, &icon) == SaveDataErrorParameter, "loadIcon null point is PARAMETER"); + Check(sceSaveDataLoadIcon(&point, &icon) == SaveDataErrorNotMounted, "loadIcon unmounted point is NOT_MOUNTED"); + Check(sceSaveDataSaveIcon(nullptr, &icon) == SaveDataErrorParameter, "saveIcon null point is PARAMETER"); + Check(sceSaveDataSaveIcon(&point, &icon) == SaveDataErrorNotMounted, "saveIcon unmounted point is NOT_MOUNTED"); + Check(sceSaveDataSaveIconByPath(&point, "test.png") == 0, "saveIconByPath succeeds"); Check(sceSaveDataGetMountInfo(nullptr, &info) == SaveDataErrorParameter, "getMountInfo null point is PARAMETER"); Check(sceSaveDataGetMountInfo(&point, nullptr) == SaveDataErrorParameter, "getMountInfo null info is PARAMETER"); Check(sceSaveDataGetMountInfo(&point, &info) == SaveDataErrorNotMounted, "getMountInfo unmounted point is NOT_MOUNTED"); 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) { SaveDataMountInfo info{}; + SaveDataIcon icon{}; char buf[128]{}; char smallBuf[64]{}; std::size_t gotSize = 0; SaveDataMountPoint unmounted{}; std::memcpy(unmounted.data, "unmounted", sizeof("unmounted")); + const char newTitle[] = "Mounted Title"; + char readTitle[128]{}; + Check(sceSaveDataUmount2(0, nullptr) == SaveDataErrorParameter, "umount2 null point is PARAMETER"); + Check(sceSaveDataLoadIcon(nullptr, &icon) == SaveDataErrorParameter, "loadIcon null point is PARAMETER"); + Check(sceSaveDataLoadIcon(point, &icon) == 0, "loadIcon on mounted save succeeds"); + Check(sceSaveDataSaveIcon(nullptr, &icon) == SaveDataErrorParameter, "saveIcon null point is PARAMETER"); + Check(sceSaveDataSaveIcon(point, &icon) == 0, "saveIcon on mounted save succeeds"); + Check(sceSaveDataSaveIconByPath(point, "icon.png") == 0, "saveIconByPath on mounted save succeeds"); 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 +90,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/GuestSaveDataSearch.cpp b/core/libs/tests/GuestSaveDataSearch.cpp index 6cea92d921..47613690d3 100644 --- a/core/libs/tests/GuestSaveDataSearch.cpp +++ b/core/libs/tests/GuestSaveDataSearch.cpp @@ -27,6 +27,16 @@ static void Require(bool value, const char* message) { if (!value) throw std::runtime_error(message); } +template +static bool ThrowsRuntimeError(TFunction function) { + try { + function(); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + static std::vector Search(std::uint32_t key, std::uint32_t order, std::uint32_t capacity, std::uint32_t* hits) { SceSaveDataDirName pattern{}; std::snprintf(pattern.data, sizeof(pattern.data), "SORT_%%"); @@ -187,7 +197,7 @@ static void SearchAndMount() { for (std::size_t i = 0; i < 2; ++i) { SaveDataSearchInfo info{}; std::memcpy(&info, infos.data() + i * 48, sizeof(info)); - Require(info.blocks == 32768 && info.free_blocks == 32768, "search info must report the save's blocks and free blocks"); + Require(info.blocks == 16384 && info.free_blocks == 16384, "search info must report the save's blocks and free blocks"); for (const auto byte : info.reserved) Require(byte == 0, "search info reserved bytes must be zero"); } std::array found{}; @@ -246,20 +256,9 @@ static std::vector SizeSearch(std::uint32_t key, std::vector return {infos.begin(), infos.end()}; } -static void SaveSize() { - CreateSized("SIZE_A", 200, 0); - CreateSized("SIZE_B", 96, 40000); - std::vector found; - auto infos = SizeSearch(0, found); - Require(found == std::vector{"SIZE_A", "SIZE_B"}, "the sized saves must sort by name"); - Require(infos[0].blocks == 200 && infos[0].free_blocks == 200, "an empty save must report all its blocks free"); - Require(infos[1].blocks == 96 && infos[1].free_blocks == 94, "a save must report the blocks it was created with, less those its files take"); - SizeSearch(2, found); - Require(found == std::vector{"SIZE_B", "SIZE_A"}, "saves must sort by blocks"); - infos = SizeSearch(5, found); - Require(found == std::vector{"SIZE_B", "SIZE_A"} && infos[0].free_blocks < infos[1].free_blocks, "saves must sort by free blocks"); +static SaveDataMountInfo MountInfo(const char* name) { SceSaveDataDirName dirName{}; - std::snprintf(dirName.data, sizeof(dirName.data), "SIZE_B"); + std::snprintf(dirName.data, sizeof(dirName.data), "%s", name); SaveDataMount3 mount{}; mount.user_id = 1; mount.dir_name = &dirName; @@ -268,8 +267,45 @@ static void SaveSize() { Require(sceSaveDataMount3(&mount, &mounted) == 0, "mounting the sized save failed"); SaveDataMountInfo info{}; Require(sceSaveDataGetMountInfo(&mounted.mount_point, &info) == 0, "mount info failed"); - Require(info.blocks == 96 && info.free_blocks == 94, "mount info must report the save's blocks and free blocks"); Require(sceSaveDataUmount2(0, &mounted.mount_point) == 0, "unmount failed"); + return info; +} + +static void SaveSize() { + CreateSized("SIZE_A", 200, 0); + CreateSized("SIZE_B", 96, 40000); + std::vector found; + auto infos = SizeSearch(0, found); + Require(found == std::vector{"SIZE_A", "SIZE_B"}, "the sized saves must sort by name"); + Require(infos[0].blocks == 200 && infos[0].free_blocks == 200, "an empty save must report all its blocks free"); + Require(infos[1].blocks == 96 && infos[1].free_blocks == 95, "a save must report the blocks it was created with, less those its files take"); + SizeSearch(2, found); + Require(found == std::vector{"SIZE_B", "SIZE_A"}, "saves must sort by blocks"); + infos = SizeSearch(5, found); + Require(found == std::vector{"SIZE_B", "SIZE_A"} && infos[0].free_blocks < infos[1].free_blocks, "saves must sort by free blocks"); + const auto info = MountInfo("SIZE_B"); + Require(info.blocks == 96 && info.free_blocks == 95, "mount info must report the save's blocks and free blocks"); +} + +static void CheckNullArguments() { + SaveDataDirNameSearchCond cond{}; + cond.user_id = 1; + SaveDataDirNameSearchResult result{}; + Require(ThrowsRuntimeError([&] { sceSaveDataDirNameSearch(nullptr, &result); }), "null cond throws in sceSaveDataDirNameSearch"); + Require(ThrowsRuntimeError([&] { sceSaveDataDirNameSearch(&cond, nullptr); }), "null result throws in sceSaveDataDirNameSearch"); + Require(ThrowsRuntimeError([&] { sceSaveDataDirNameSearch2(nullptr, &result); }), "null cond throws in sceSaveDataDirNameSearch2"); + Require(ThrowsRuntimeError([&] { sceSaveDataDirNameSearch2(&cond, nullptr); }), "null result throws in sceSaveDataDirNameSearch2"); + cond.order = 2; + Require(sceSaveDataDirNameSearch2(&cond, &result) == SaveDataErrorParameter, "invalid order must be rejected in sceSaveDataDirNameSearch2"); +} + +static void BlockSize() { + CreateSized("BLOCK_A", 48, 70000); + auto info = MountInfo("BLOCK_A"); + Require(info.blocks == 48 && info.free_blocks == 46, "a file must take whole 64 KiB blocks"); + CreateSized("BLOCK_B", 32768, 0); + info = MountInfo("BLOCK_B"); + Require(info.blocks == 16384 && info.free_blocks == 16384, "a save must hold at most 16384 blocks"); } int main() { @@ -285,6 +321,8 @@ int main() { OtherTitle(); TrailingWildcards(); SaveSize(); + BlockSize(); + CheckNullArguments(); } catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); status = 1; diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index a67eccf249..b3499c1492 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -5,12 +5,14 @@ #include #else #include +#include #include #include #include #endif #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libkernel/Socket/include/SocketSelectDeadline.hpp" +#include "prx/libkernel/Time/include/Time.hpp" #include "SceTypes.hpp" #include #include @@ -24,6 +26,7 @@ #include #include #include +#include extern "C" { extern const std::uint8_t in6addr_any_nid_postfix[16]; @@ -32,11 +35,13 @@ int APS5_VABI sceNetInit_nid_postfix(void); int APS5_VABI sceNetShowNetstatWithMemory(int); int APS5_VABI sceNetShowNetstat(void); int APS5_VABI sceNetShowIfconfig(void); +int APS5_VABI sceNetShowRoute(void); int APS5_VABI sceNetSocket(const char*, int, int, int); 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); @@ -63,10 +68,14 @@ 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 sceNetGetDnsInfo(std::uint32_t*, int); int APS5_VABI sceNetCtlGetState(int*); +int APS5_VABI sceNetCtlGetInfo(int, NetCtlInfo*); int APS5_VABI sceNetCtlGetIfStat(NetCtlIfStat*); int APS5_VABI sceNetCtlRegisterCallback(NetCtlCallback, void*, int*); int APS5_VABI sceNetCtlUnregisterCallback(int); @@ -89,6 +98,9 @@ struct GuestPollEntry { int APS5_VABI poll_nid_postfix(GuestPollEntry*, std::uint32_t, int); int* APS5_VABI __error_nid_postfix(); int APS5_VABI sceNetInetPton(int, const char*, void*); +int APS5_VABI sceNetInetPtonEx(int, const char*, void*, int); +int APS5_VABI sceNetUsleep(int); +void APS5_VABI sceNetGetSystemTime(std::uint64_t*); const char* APS5_VABI sceNetInetNtop(int, const void*, char*, std::uint32_t); int* APS5_VABI sceNetErrnoLoc(void); } @@ -120,6 +132,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) { @@ -132,6 +148,10 @@ static bool Failed(std::int64_t result, int error) { return result == static_cast(0x80410100u | static_cast(error)) && *sceNetErrnoLoc() == error; } +static void CheckSocketHostErrors() { + Require(Failed(sceNetSocket(nullptr, 2, 1, 17), 43)); +} + static void CheckStreamSender(int family) { const auto addressSize = family == 2 ? 16u : 28u; const int listener = sceNetSocket(nullptr, family, 1, 6); @@ -220,6 +240,7 @@ static std::string CaptureNetstat(int pool) { static void CheckNetstat() { Require(sceNetShowNetstat() == 0); Require(sceNetShowIfconfig() == 0); + Require(sceNetShowRoute() == 0); Require(Failed(sceNetShowNetstatWithMemory(-1), 9)); const int pool = sceNetPoolCreate("netstat", 4096, 0); Require(pool > 0); @@ -256,6 +277,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}) { @@ -462,13 +527,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)); @@ -485,7 +552,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); @@ -715,6 +781,121 @@ static void CheckAddressText(int family, const char* text) { Require(sceNetInetNtop(family, address.data(), nullptr, output.size()) == nullptr && *sceNetErrnoLoc() == 22); } +static void CheckInetPtonRejects(int family, const char* text) { + std::array address{}; + address.fill(0xaa); + *sceNetErrnoLoc() = 123; + Require(sceNetInetPton(family, text, address.data()) == 0 && *sceNetErrnoLoc() == 123); + Require(std::all_of(address.begin(), address.end(), [](std::uint8_t byte) { return byte == 0xaa; })); +} + +static void CheckInetPtonWhitespace() { + for (const char* text : {"1.2.3.4 ", "1.2.3.4\t", "1.2.3.4\n", "1.2.3.4\v", "1.2.3.4\f", "1.2.3.4\r", "1.2.3.4 5", "1.2.3.4 x"}) { + std::array address{}; + address.fill(0xaa); + *sceNetErrnoLoc() = 123; + Require(sceNetInetPton(2, text, address.data()) == 1 && *sceNetErrnoLoc() == 123); + Require(address[0] == 1 && address[1] == 2 && address[2] == 3 && address[3] == 4 && address[4] == 0xaa); + } + for (const char* text : {" 1.2.3.4", "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", "1.2.3 ", "01.2.3.4", "256.0.0.1"}) { + CheckInetPtonRejects(2, text); + } + for (const char* text : {"::1 ", "::1\t", " ::1", "::1x"}) { + CheckInetPtonRejects(28, text); + } +} + +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); + Require(address[0] == 192 && address[1] == 168 && address[2] == 1 && address[3] == 20); + std::array expected{}; + Require(sceNetInetPton(28, "fe80::1:2", expected.data()) == 1); + address.fill(0); + Require(sceNetInetPtonEx(28, "fe80::1:2", address.data(), 0) == 1 && address == expected); + Require(sceNetInetPtonEx(2, "not an address", address.data(), 0) == 0); + 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, 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); + } +} + +static void CheckSystemTime() { + *sceNetErrnoLoc() = 22; + sceNetGetSystemTime(nullptr); + Require(*sceNetErrnoLoc() == 22); + std::array output{0x123456789abcdef0ULL, UINT64_MAX, 0xfedcba9876543210ULL}; + std::uint64_t previous = 0; + for (int i = 0; i < 64; ++i) { + output[1] = UINT64_MAX; + const auto before = sceKernelGetProcessTime(); + sceNetGetSystemTime(&output[1]); + const auto after = sceKernelGetProcessTime(); + Require(before <= output[1] && output[1] <= after && previous <= output[1]); + Require(output[0] == 0x123456789abcdef0ULL && output[2] == 0xfedcba9876543210ULL); + Require(*sceNetErrnoLoc() == 22); + previous = output[1]; + } + std::this_thread::sleep_for(std::chrono::milliseconds(2)); + sceNetGetSystemTime(&output[1]); + Require(output[1] > previous); +} + +static void CheckUsleep() { + Require(sceNetUsleep(0) == 0); + const auto start = std::chrono::steady_clock::now(); + Require(sceNetUsleep(2000) == 0); + Require(std::chrono::steady_clock::now() - start >= std::chrono::microseconds(2000)); + *sceNetErrnoLoc() = 0; + Require(Failed(sceNetUsleep(-1), 22)); +} + static void CheckEpollIdent() { std::array address{16, 2, 0, 0, 127, 0, 0, 1}; const int receiver = sceNetSocket("epoll-ident", 2, 2, 17); @@ -741,6 +922,27 @@ static void CheckEpollIdent() { Require(sceNetSocketClose(sender) == 0 && sceNetSocketClose(receiver) == 0); } +static void CheckTcpNoDelay() { + const int stream = sceNetSocket("tcp-nodelay", 2, 1, 6); + const int datagram = sceNetSocket("tcp-nodelay-udp", 2, 2, 17); + Require(stream >= 0 && datagram >= 0); + int value = -1; + std::uint32_t size = sizeof(value); + Require(sceNetGetsockopt(stream, 6, 1, &value, &size) == 0 && size == sizeof(value) && value == 0); + const int on = 1; + const int off = 0; + Require(sceNetSetsockopt(stream, 6, 1, &on, sizeof(on)) == 0); + Require(sceNetGetsockopt(stream, 6, 1, &value, &size) == 0 && value == 4); + Require(setsockopt_nid_postfix(stream, 6, 1, &off, sizeof(off)) == 0); + Require(sceNetGetsockopt(stream, 6, 1, &value, &size) == 0 && value == 0); + Require(Failed(sceNetSetsockopt(stream, 6, 2, &on, sizeof(on)), 42)); + Require(Failed(sceNetGetsockopt(stream, 6, 2, &value, &size), 42)); + Require(Failed(sceNetSetsockopt(datagram, 6, 1, &on, sizeof(on)), 22)); + Require(Failed(sceNetGetsockopt(datagram, 6, 1, &value, &size), 22)); + Require(Failed(sceNetSetsockopt(datagram, 6, 2, &on, sizeof(on)), 22)); + Require(sceNetSocketClose(stream) == 0 && sceNetSocketClose(datagram) == 0); +} + static bool ipv6Unavailable = false; static bool HostIpv6LoopbackUnavailable() { @@ -937,7 +1139,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; @@ -1080,6 +1282,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); @@ -1131,7 +1439,11 @@ int main() { } Require(sceNetInit_nid_postfix() == 0); CheckEpollWritable(); + CheckSocketHostErrors(); CheckPoolStats(); + CheckIfName(); + CheckRandom(); + CheckNetMemory(); CheckNetstat(); CheckEmptyEpoll(); CheckDontWait(); @@ -1139,7 +1451,13 @@ int main() { CheckAddressText(2, "255.255.255.255"); CheckAddressText(28, "::1"); CheckAddressText(28, "1234:5678:9abc:def0:1234:5678:9abc:def0"); + CheckInetPtonWhitespace(); + CheckInetPtonExParts(); + CheckInetPtonEx(); + CheckUsleep(); + CheckSystemTime(); CheckEpollIdent(); + CheckTcpNoDelay(); CheckLinkLocalAllNodesIpv6(); CheckUnboundSocketName(); CheckUnboundDatagramReceive(); @@ -1155,6 +1473,7 @@ int main() { } CheckDatagramDisconnect(); CheckCloseKernelSocket(); + CheckSockInfo(); const int closed = sceNetSocket("closed-by-kernel", 2, 2, 17); Require(closed >= 0); @@ -1443,6 +1762,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); @@ -1451,6 +1817,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) && @@ -1490,6 +1865,34 @@ int main() { } Require(missing); } + { + std::array dns{}; + dns.fill(0xA5A5A5A5u); + const int count = sceNetGetDnsInfo(dns.data(), 0); + Require(count >= 0 && count <= 2 && dns[2] == 0xA5A5A5A5u); + Require(count == 0 || state == 3); + for (int index = count; index < 2; ++index) Require(dns[index] == 0); + for (int index = 0; index < 2; ++index) { + NetCtlInfo info{}; + const int result = sceNetCtlGetInfo(17 + index, &info); + if (result != 0) { + Require(result == static_cast(0x80412108) && count == 0); + continue; + } + const auto* bytes = reinterpret_cast(&dns[index]); + char text[16] = {}; + std::snprintf(text, sizeof(text), "%u.%u.%u.%u", static_cast(bytes[0]), static_cast(bytes[1]), static_cast(bytes[2]), static_cast(bytes[3])); + Require(std::strcmp(text, reinterpret_cast(info.opaque)) == 0); + } + for (const int flags : {0x1, -1}) { + bool threw = false; + try { sceNetGetDnsInfo(dns.data(), flags); } catch (const std::runtime_error&) { threw = true; } + Require(threw); + } + bool threw = false; + try { sceNetGetDnsInfo(nullptr, 0); } catch (const std::runtime_error&) { threw = true; } + Require(threw); + } NetCtlIfStat first{}; first.device = 7; first.reserved[7] = 7; diff --git a/core/libs/tests/GuestSetrlimit.cpp b/core/libs/tests/GuestSetrlimit.cpp index 383b48d06f..a869eec59c 100644 --- a/core/libs/tests/GuestSetrlimit.cpp +++ b/core/libs/tests/GuestSetrlimit.cpp @@ -19,6 +19,10 @@ int* APS5_VABI __error_nid_postfix(); static constexpr int GUEST_EFAULT = 14; static constexpr int GUEST_EINVAL = 22; static constexpr int GUEST_RLIMIT_DATA = 2; +static constexpr int GUEST_RLIMIT_STACK = 3; +static constexpr int GUEST_RLIMIT_RSS = 5; +static constexpr int GUEST_RLIMIT_MEMLOCK = 6; +static constexpr int GUEST_RLIMIT_NPROC = 7; static constexpr int GUEST_RLIMIT_NOFILE = 8; static constexpr int GUEST_RLIM_NLIMITS = 15; static constexpr std::int64_t GUEST_RLIM_INFINITY = std::numeric_limits::max(); @@ -47,6 +51,20 @@ int main() { RequireError(-1, &infinity, GUEST_EINVAL); RequireError(GUEST_RLIM_NLIMITS, &infinity, GUEST_EINVAL); + static constexpr int unchanged[] = {GUEST_RLIMIT_STACK, GUEST_RLIMIT_RSS, GUEST_RLIMIT_MEMLOCK}; + for (int resource : unchanged) { + ResourceLimit before{}; + Require(getrlimit_nid_postfix(resource, &before) == 0); + *__error_nid_postfix() = 13; + Require(setrlimit_nid_postfix(resource, &before) == 0 && *__error_nid_postfix() == 13); + ResourceLimit after{}; + Require(getrlimit_nid_postfix(resource, &after) == 0 && after.rlim_cur == before.rlim_cur && after.rlim_max == before.rlim_max); + } + ResourceLimit processes{}; + Require(getrlimit_nid_postfix(GUEST_RLIMIT_NPROC, &processes) == 0 && processes.rlim_cur == 1604 && processes.rlim_max == 1604); + Require(Throws(GUEST_RLIMIT_NPROC, processes)); + Require(Throws(GUEST_RLIMIT_STACK, ResourceLimit{0x100000, 0x200000})); + ResourceLimit current{}; Require(getrlimit_nid_postfix(GUEST_RLIMIT_DATA, ¤t) == 0); *__error_nid_postfix() = 13; diff --git a/core/libs/tests/GuestSignals.cpp b/core/libs/tests/GuestSignals.cpp index 016c4c5a0f..5183ec41b0 100644 --- a/core/libs/tests/GuestSignals.cpp +++ b/core/libs/tests/GuestSignals.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include using Handler = void (APS5_VABI *)(int); @@ -14,6 +15,8 @@ struct GuestSignalSet { extern "C" { Handler APS5_VABI signal_nid_postfix(int, Handler); int APS5_VABI raise_nid_postfix(int); +int APS5_VABI kill_nid_postfix(int, int); +int APS5_VABI getpid_nid_postfix(); int APS5_VABI pthread_kill_nid_postfix(void*, int); void* APS5_VABI scePthreadSelf(); int APS5_VABI scePthreadCreate(void** thread, const void* attr, void* (APS5_VABI *entry)(void*), void* arg, const char* name); @@ -54,6 +57,14 @@ struct SuspendSender { void* target = nullptr; int result = -1; }; +struct EndingThread { + std::atomic release{false}; +}; +void* APS5_VABI EndingEntry(void* arg) { + auto& state = *static_cast(arg); + while (!state.release.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + return nullptr; +} void* APS5_VABI SuspendSend(void* arg) { auto& sender = *static_cast(arg); std::this_thread::sleep_for(std::chrono::milliseconds(50)); @@ -87,12 +98,32 @@ int main() { Require(pthread_kill_nid_postfix(nullptr, 128) == 3); Require(pthread_kill_nid_postfix(nullptr, 129) == 22); Require(pthread_kill_nid_postfix(reinterpret_cast(std::uintptr_t{0x10}), 15) == 3); + Require(pthread_kill_nid_postfix(reinterpret_cast(std::uintptr_t{0x10}), 0) == 3); + { + EndingThread ending; + void* thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, EndingEntry, &ending, nullptr) == 0); + Require(pthread_kill_nid_postfix(thread, 0) == 0); + ending.release.store(true); + int answer = 0; + for (int attempt = 0; attempt < 2000 && (answer = pthread_kill_nid_postfix(thread, 0)) != 3; ++attempt) + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + Require(answer == 3); + Require(pthread_kill_nid_postfix(thread, 15) == 3); + Require(scePthreadJoin(thread, nullptr) == 0); + Require(pthread_kill_nid_postfix(thread, 0) == 3); + } Require(received == 0); Require(signal_nid_postfix(15, ignore) != invalid); received = 0; Require(raise_nid_postfix(15) == 0 && received == 0); Require(signal_nid_postfix(15, nullptr) == ignore); Require(raise_nid_postfix(100) == -1 && *__error_nid_postfix() == 22); + *__error_nid_postfix() = 0; + received = 0; + Require(raise_nid_postfix(0) == 0 && received == 0 && *__error_nid_postfix() == 0); + Require(raise_nid_postfix(129) == -1 && *__error_nid_postfix() == 22); + Require(raise_nid_postfix(-1) == -1 && *__error_nid_postfix() == 22); GuestSignalSet blocked{{0x20, 0, 0, 0}}; GuestSignalSet previous{{}}; Require(sigprocmask_nid_postfix(3, &blocked, &previous) == 0); @@ -310,4 +341,28 @@ int main() { pendingSignals.bits[0] == 0 && pendingSignals.bits[1] == 0 && pendingSignals.bits[2] == 0 && pendingSignals.bits[3] == 0); for (int guest : {40, 73, 128}) Require(signal_nid_postfix(guest, nullptr) == Callback); + for (const int ignoredByDefault : {16, 19, 20, 23, 28, 29}) { + *__error_nid_postfix() = 0; + received = 0; + Require(raise_nid_postfix(ignoredByDefault) == 0 && received == 0 && *__error_nid_postfix() == 0); + Require(kill_nid_postfix(getpid_nid_postfix(), ignoredByDefault) == 0 && *__error_nid_postfix() == 0); + } + const GuestSignalSet ignoredByDefaultSet{{(1u << 15) | (1u << 19) | (1u << 22) | (1u << 27) | (1u << 28), 0, 0, 0}}; + GuestSignalSet pendingIgnored{}; + Require(sigprocmask_nid_postfix(3, &ignoredByDefaultSet, nullptr) == 0); + for (const int ignoredByDefault : {16, 20, 23, 28, 29}) Require(raise_nid_postfix(ignoredByDefault) == 0); + Require(sigpending_nid_postfix(&pendingIgnored) == 0 && pendingIgnored.bits[0] == 0); + const GuestSignalSet noneBlocked{{0, 0, 0, 0}}; + Require(sigprocmask_nid_postfix(3, &noneBlocked, nullptr) == 0); + const GuestSigaction ignored{1, 0, {}}; + const GuestSigaction restored{0, 0, {}}; + const GuestSignalSet userSignal{{1u << 29, 0, 0, 0}}; + Require(sigaction_nid_postfix(30, &ignored, nullptr) == 0); + *__error_nid_postfix() = 0; + Require(raise_nid_postfix(30) == 0 && kill_nid_postfix(getpid_nid_postfix(), 30) == 0 && *__error_nid_postfix() == 0); + Require(sigprocmask_nid_postfix(3, &userSignal, nullptr) == 0); + Require(raise_nid_postfix(30) == 0); + Require(sigpending_nid_postfix(&pendingIgnored) == 0 && pendingIgnored.bits[0] == 0); + Require(sigprocmask_nid_postfix(3, &noneBlocked, nullptr) == 0); + Require(sigaction_nid_postfix(30, &restored, nullptr) == 0); } diff --git a/core/libs/tests/GuestSigtimedwait.cpp b/core/libs/tests/GuestSigtimedwait.cpp index 52a65f57d5..0663a3ce2f 100644 --- a/core/libs/tests/GuestSigtimedwait.cpp +++ b/core/libs/tests/GuestSigtimedwait.cpp @@ -1,5 +1,6 @@ #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include @@ -79,6 +80,44 @@ static Pthread StartSenderWithoutDelay(Sender& sender) { static void JoinSender(Pthread thread, const Sender& sender) { Require(scePthreadJoin(thread, nullptr) == 0 && sender.result == 0); } +struct TimeoutSender { + Pthread target; + std::atomic finished{false}; +}; +static void* APS5_VABI SendUnrelatedUntilTimeout(void* arg) { + auto& sender = *static_cast(arg); + for (int round = 0; round < 80 && !sender.finished.load(); ++round) { + Require(pthread_kill_nid_postfix(sender.target, 2) == 0); + std::this_thread::sleep_for(std::chrono::milliseconds(25)); + } + if (!sender.finished.load()) Require(pthread_kill_nid_postfix(sender.target, 13) == 0); + return nullptr; +} +static void CheckTimeoutWithUnrelatedSignals() { + const GuestSignalSet blocked{{(1u << 1) | (1u << 12), 0, 0, 0}}; + const GuestSignalSet pipe{{1u << 12, 0, 0, 0}}; + const GuestSignalSet interrupt{{1u << 1, 0, 0, 0}}; + const GuestTimespec timeout{0, 200000000}; + const GuestTimespec poll{0, 0}; + GuestSignalSet previous{}; + Require(_sigprocmask_nid_postfix(1, &blocked, &previous) == 0); + TimeoutSender sender{scePthreadSelf()}; + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, SendUnrelatedUntilTimeout, &sender, nullptr) == 0); + GuestSiginfo info{}; + const auto begin = std::chrono::steady_clock::now(); + const int result = sigtimedwait_nid_postfix(&pipe, &info, &timeout); + const int error = *__error_nid_postfix(); + const auto elapsed = std::chrono::steady_clock::now() - begin; + sender.finished.store(true); + Require(scePthreadJoin(thread, nullptr) == 0); + Require(result == -1 && error == 35); + Require(elapsed >= std::chrono::milliseconds(200)); + RequireUntouched(info); + Require(sigtimedwait_nid_postfix(&interrupt, nullptr, &poll) == 2); + Require(sigtimedwait_nid_postfix(&pipe, nullptr, &poll) == -1 && *__error_nid_postfix() == 35); + Require(_sigprocmask_nid_postfix(3, &previous, nullptr) == 0); +} int main() { const GuestSignalSet empty{{0, 0, 0, 0}}; const GuestSignalSet pipe{{1u << 12, 0, 0, 0}}; @@ -149,6 +188,10 @@ int main() { senderThread = StartSender(sender); Require(sigtimedwait_nid_postfix(&pipe, nullptr, nullptr) == 13); JoinSender(senderThread, sender); + senderThread = StartSender(sender); + const GuestTimespec patience{1, 0}; + Require(sigtimedwait_nid_postfix(&pipe, &info, &patience) == 13 && info.signo == 13); + JoinSender(senderThread, sender); Require(signal_nid_postfix(15, Callback) != reinterpret_cast(static_cast(-1))); senderThread = StartSender(sender = Sender{self, 15, -1}); received = 0; @@ -169,4 +212,5 @@ int main() { JoinSender(senderThread, tight); } Require(_sigprocmask_nid_postfix(3, &previous, nullptr) == 0); + CheckTimeoutWithUnrelatedSignals(); } diff --git a/core/libs/tests/GuestSleepCancel.cpp b/core/libs/tests/GuestSleepCancel.cpp new file mode 100644 index 0000000000..463a4a0cbe --- /dev/null +++ b/core/libs/tests/GuestSleepCancel.cpp @@ -0,0 +1,108 @@ +#include "SceTypes.hpp" +#include "prx/libkernel/Pthread/include/Pthread.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); +int APS5_VABI scePthreadJoin(Pthread thread, void** result); +int APS5_VABI scePthreadCancel(Pthread thread); +int APS5_VABI scePthreadSetcancelstate(int state, int* oldState); +void APS5_VABI scePthreadTestcancel(); +unsigned int APS5_VABI sleep_nid_postfix(unsigned int seconds); +} + +static void Require(bool value, std::source_location location = std::source_location::current()) { + if (value) return; + std::fprintf(stderr, "%s:%u: requirement failed\n", location.file_name(), location.line()); + std::abort(); +} + +static constexpr auto ObservationLimit = std::chrono::seconds(3); +static constexpr unsigned LongSleep = 10; +static std::atomic ready{false}; +static std::atomic release{false}; +static std::atomic returned{false}; +static bool disabled = false; +static bool pending = false; +static unsigned seconds = LongSleep; +static std::chrono::steady_clock::duration elapsed{}; + +static void* APS5_VABI Sleeper(void*) { + if (disabled) Require(scePthreadSetcancelstate(1, nullptr) == 0); + ready.store(true); + ready.notify_all(); + if (pending) release.wait(false); + const auto start = std::chrono::steady_clock::now(); + Require(sleep_nid_postfix(seconds) == 0); + elapsed = std::chrono::steady_clock::now() - start; + returned.store(true); + if (disabled) { + Require(scePthreadSetcancelstate(0, nullptr) == 0); + scePthreadTestcancel(); + Require(false); + } + return nullptr; +} + +static bool WaitForCompletion(Pthread thread) { + const auto limit = std::chrono::steady_clock::now() + ObservationLimit; + while (!thread->_finished.load(std::memory_order_acquire)) { + if (std::chrono::steady_clock::now() >= limit) return false; + std::this_thread::yield(); + } + return true; +} + +static bool WaitForSleep(Pthread thread) { + const auto limit = std::chrono::steady_clock::now() + ObservationLimit; + for (;;) { + { + std::lock_guard lock(thread->cancelLock); + if (thread->cancelWait != nullptr) return true; + } + if (std::chrono::steady_clock::now() >= limit) return false; + std::this_thread::yield(); + } +} + +int main(int count, char** arguments) { + Require(count == 2); + const std::string_view mode(arguments[1]); + disabled = mode == "disabled"; + const bool zero = mode == "pending"; + pending = zero || disabled; + const bool normal = mode == "normal"; + Require(mode == "blocked" || disabled || pending || normal); + if (disabled || normal) seconds = 1; + if (zero) seconds = 0; + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, Sleeper, nullptr, nullptr) == 0); + ready.wait(false); + bool wasSleeping = false; + if (!normal) { + if (!pending) wasSleeping = WaitForSleep(thread); + Require(scePthreadCancel(thread) == 0); + release.store(true); + release.notify_all(); + } + const bool completed = WaitForCompletion(thread); + void* result = nullptr; + Require(scePthreadJoin(thread, &result) == 0); + if (!completed) std::fputs("sleep stayed blocked after cancellation\n", stderr); + Require(completed); + if (normal) { + Require(result == nullptr && returned.load()); + } else { + Require(result == reinterpret_cast(std::uintptr_t{1})); + Require(returned.load() == disabled); + if (!pending) Require(wasSleeping); + } + if (normal || disabled) Require(elapsed >= std::chrono::seconds(1)); +} diff --git a/core/libs/tests/GuestSleepWake.cpp b/core/libs/tests/GuestSleepWake.cpp new file mode 100644 index 0000000000..177ef4b990 --- /dev/null +++ b/core/libs/tests/GuestSleepWake.cpp @@ -0,0 +1,130 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceKernelUsleep_nid_postfix(KernelUseconds microseconds); +void KernelWakeSleepers_nid_no_patch(); +void KernelNoteGpuSubmit_nid_no_patch(); +} + +static constexpr KernelUseconds WAKEABLE_MICROS = 1000; +static constexpr KernelUseconds LONG_MICROS = 2000; +static constexpr unsigned EARLY_WAKES_PER_RUN = 8; +static constexpr unsigned TRIES = 20; +static constexpr auto EARLY = std::chrono::microseconds(900); + +static void Require(bool value) { if (!value) std::abort(); } + +static std::chrono::microseconds Usleep(KernelUseconds microseconds) { + const auto start = std::chrono::steady_clock::now(); + Require(sceKernelUsleep_nid_postfix(microseconds) == 0); + return std::chrono::duration_cast(std::chrono::steady_clock::now() - start); +} + +static void Idle(std::chrono::microseconds length) { + const auto until = std::chrono::steady_clock::now() + length; + while (std::chrono::steady_clock::now() < until) std::this_thread::yield(); +} + +class Waker { +public: + Waker() : thread([this] { + while (!stop.load()) { + KernelWakeSleepers_nid_no_patch(); + std::this_thread::sleep_for(std::chrono::microseconds(50)); + } + }) {} + ~Waker() { + stop.store(true); + thread.join(); + } + +private: + std::atomic stop{false}; + std::thread thread; +}; + +#ifdef _WIN32 +static void CoarseWaitsKeepEverySleepFull() { + Waker waker; + for (unsigned i = 0; i < 12; ++i) { + KernelNoteGpuSubmit_nid_no_patch(); + Require(Usleep(WAKEABLE_MICROS) >= std::chrono::microseconds(WAKEABLE_MICROS)); + } +} +#endif + +static void SleepWithoutWakeIsFull() { + for (unsigned i = 0; i < 4; ++i) Require(Usleep(WAKEABLE_MICROS) >= std::chrono::microseconds(WAKEABLE_MICROS)); +} + +static void WakeEndsShortSleep() { + Waker waker; + bool early = false; + for (unsigned i = 0; i < TRIES && !early; ++i) { + KernelNoteGpuSubmit_nid_no_patch(); + early = Usleep(WAKEABLE_MICROS) < EARLY; + } + Require(early); +} + +static void WakeLeavesLongSleep() { + Waker waker; + for (unsigned i = 0; i < 4; ++i) { + KernelNoteGpuSubmit_nid_no_patch(); + Require(Usleep(LONG_MICROS) >= std::chrono::microseconds(LONG_MICROS)); + } +} + +static void RunOfSleepsEndsEarlyAtMostEightTimes() { + Waker waker; + KernelNoteGpuSubmit_nid_no_patch(); + unsigned early = 0; + for (unsigned i = 0; i < EARLY_WAKES_PER_RUN + 8; ++i) { + if (Usleep(WAKEABLE_MICROS) < std::chrono::microseconds(WAKEABLE_MICROS)) ++early; + } + Require(early <= EARLY_WAKES_PER_RUN); + for (unsigned i = 0; i < 4; ++i) Require(Usleep(WAKEABLE_MICROS) >= std::chrono::microseconds(WAKEABLE_MICROS)); +} + +static void SubmitStartsNewRun() { + Waker waker; + for (unsigned i = 0; i < EARLY_WAKES_PER_RUN + 4; ++i) Usleep(WAKEABLE_MICROS); + bool early = false; + for (unsigned i = 0; i < TRIES && !early; ++i) { + KernelNoteGpuSubmit_nid_no_patch(); + early = Usleep(WAKEABLE_MICROS) < EARLY; + } + Require(early); +} + +static void GapStartsNewRun() { + Waker waker; + for (unsigned i = 0; i < EARLY_WAKES_PER_RUN + 4; ++i) Usleep(WAKEABLE_MICROS); + bool early = false; + for (unsigned i = 0; i < TRIES && !early; ++i) { + Idle(std::chrono::microseconds(WAKEABLE_MICROS + 200)); + early = Usleep(WAKEABLE_MICROS) < EARLY; + } + Require(early); +} + +int main() { +#ifdef _WIN32 + if (std::getenv("APS5_COARSE_TIMED_WAITS") != nullptr) { + CoarseWaitsKeepEverySleepFull(); + return 0; + } +#endif + SleepWithoutWakeIsFull(); + WakeEndsShortSleep(); + WakeLeavesLongSleep(); + RunOfSleepsEndsEarlyAtMostEightTimes(); + SubmitStartsNewRun(); + GapStartsNewRun(); + return 0; +} 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/GuestSocketDescriptorFlags.cpp b/core/libs/tests/GuestSocketDescriptorFlags.cpp index a8c1ba10f2..ef8772f738 100644 --- a/core/libs/tests/GuestSocketDescriptorFlags.cpp +++ b/core/libs/tests/GuestSocketDescriptorFlags.cpp @@ -8,17 +8,24 @@ int APS5_VABI socket_nid_postfix(int, int, int); int APS5_VABI close_nid_postfix(int); int APS5_VABI dup_nid_postfix(int); int APS5_VABI dup2_nid_postfix(int, int); +int APS5_VABI pipe_nid_postfix(int*); int APS5_VABI fcntl_nid_postfix(int, int, ...); int* APS5_VABI __error_nid_postfix(); } namespace { +constexpr int DuplicateDescriptor = 0; constexpr int GetDescriptorFlags = 1; constexpr int SetDescriptorFlags = 2; +constexpr int Duplicate2Descriptor = 10; +constexpr int DuplicateDescriptorCloseOnExec = 17; +constexpr int Duplicate2DescriptorCloseOnExec = 18; constexpr int CloseOnExec = 1; constexpr int Stream = 1; constexpr int SocketCloseOnExec = 0x10000000; +constexpr int GuestEbadf = 9; +constexpr int GuestEinval = 22; void Require(bool condition, const char* what) { if (condition) return; @@ -51,6 +58,47 @@ int main() { Require(fcntl_nid_postfix(plain, SetDescriptorFlags, 0) == 0, "F_SETFD 0 must clear FD_CLOEXEC"); Require(close_nid_postfix(target) == 0, "close failed"); + Require(fcntl_nid_postfix(plain, SetDescriptorFlags, CloseOnExec) == 0, "F_SETFD on a socket must succeed"); + const int minimumDup = fcntl_nid_postfix(plain, DuplicateDescriptor, plain + 4); + Require(minimumDup >= plain + 4 && fcntl_nid_postfix(minimumDup, GetDescriptorFlags) == 0, "F_DUPFD must honor the minimum and clear FD_CLOEXEC"); + Require(fcntl_nid_postfix(plain, GetDescriptorFlags) == CloseOnExec, "F_DUPFD must leave the flag of the original"); + Require(fcntl_nid_postfix(plain, SetDescriptorFlags, 0) == 0, "F_SETFD 0 must clear FD_CLOEXEC"); + const int cloexecDup = fcntl_nid_postfix(plain, DuplicateDescriptorCloseOnExec, minimumDup + 1); + Require(cloexecDup >= minimumDup + 1 && fcntl_nid_postfix(cloexecDup, GetDescriptorFlags) == CloseOnExec, "F_DUPFD_CLOEXEC must honor the minimum and set FD_CLOEXEC"); + Require(fcntl_nid_postfix(plain, GetDescriptorFlags) == 0, "F_DUPFD_CLOEXEC must leave the flag of the original"); + Require(fcntl_nid_postfix(plain, DuplicateDescriptor, -1) == -1 && *__error_nid_postfix() == GuestEinval, "F_DUPFD with a negative minimum must fail with EINVAL"); + Require(fcntl_nid_postfix(plain, DuplicateDescriptorCloseOnExec, -1) == -1 && *__error_nid_postfix() == GuestEinval, "F_DUPFD_CLOEXEC with a negative minimum must fail with EINVAL"); + Require(close_nid_postfix(cloexecDup) == 0 && close_nid_postfix(minimumDup) == 0, "close failed"); + + const int dup2Target = socket_nid_postfix(2, Stream, 0); + Require(dup2Target >= 0, "socket failed"); + Require(fcntl_nid_postfix(dup2Target, SetDescriptorFlags, CloseOnExec) == 0 && fcntl_nid_postfix(plain, SetDescriptorFlags, CloseOnExec) == 0, "F_SETFD on a socket must succeed"); + Require(fcntl_nid_postfix(plain, Duplicate2Descriptor, dup2Target) == dup2Target, "F_DUP2FD onto another socket must return the target"); + Require(fcntl_nid_postfix(dup2Target, GetDescriptorFlags) == 0, "F_DUP2FD must clear FD_CLOEXEC on the target"); + Require(fcntl_nid_postfix(plain, GetDescriptorFlags) == CloseOnExec, "F_DUP2FD must leave the flag of the original"); + Require(fcntl_nid_postfix(plain, Duplicate2Descriptor, plain) == plain && fcntl_nid_postfix(plain, GetDescriptorFlags) == CloseOnExec, "F_DUP2FD onto the same descriptor must preserve FD_CLOEXEC"); + Require(fcntl_nid_postfix(plain, SetDescriptorFlags, 0) == 0, "F_SETFD 0 must clear FD_CLOEXEC"); + Require(fcntl_nid_postfix(plain, Duplicate2Descriptor, plain) == plain && fcntl_nid_postfix(plain, GetDescriptorFlags) == 0, "F_DUP2FD onto the same descriptor must preserve a cleared flag"); + Require(fcntl_nid_postfix(plain, Duplicate2DescriptorCloseOnExec, dup2Target) == dup2Target, "F_DUP2FD_CLOEXEC onto another socket must return the target"); + Require(fcntl_nid_postfix(dup2Target, GetDescriptorFlags) == CloseOnExec, "F_DUP2FD_CLOEXEC must set FD_CLOEXEC on the target"); + Require(fcntl_nid_postfix(plain, GetDescriptorFlags) == 0, "F_DUP2FD_CLOEXEC must leave the flag of the original"); + Require(fcntl_nid_postfix(plain, Duplicate2DescriptorCloseOnExec, plain) == plain && fcntl_nid_postfix(plain, GetDescriptorFlags) == CloseOnExec, "F_DUP2FD_CLOEXEC onto the same descriptor must set FD_CLOEXEC"); + Require(fcntl_nid_postfix(plain, SetDescriptorFlags, 0) == 0, "F_SETFD 0 must clear FD_CLOEXEC"); + Require(fcntl_nid_postfix(plain, Duplicate2Descriptor, -1) == -1 && *__error_nid_postfix() == GuestEbadf, "F_DUP2FD with a negative target must fail with EBADF"); + Require(fcntl_nid_postfix(plain, Duplicate2DescriptorCloseOnExec, -1) == -1 && *__error_nid_postfix() == GuestEbadf, "F_DUP2FD_CLOEXEC with a negative target must fail with EBADF"); + Require(close_nid_postfix(dup2Target) == 0, "close failed"); + + int pipeDescriptors[2] = {-1, -1}; + Require(pipe_nid_postfix(pipeDescriptors) == 0, "pipe failed"); + Require(fcntl_nid_postfix(pipeDescriptors[0], SetDescriptorFlags, CloseOnExec) == 0, "F_SETFD on a pipe must succeed"); + Require(fcntl_nid_postfix(plain, SetDescriptorFlags, CloseOnExec) == 0, "F_SETFD on a socket must succeed"); + Require(fcntl_nid_postfix(plain, Duplicate2Descriptor, pipeDescriptors[0]) == pipeDescriptors[0], "F_DUP2FD onto a pipe must return the target"); + Require(fcntl_nid_postfix(pipeDescriptors[0], GetDescriptorFlags) == 0, "F_DUP2FD onto a pipe must clear FD_CLOEXEC"); + Require(fcntl_nid_postfix(plain, SetDescriptorFlags, 0) == 0, "F_SETFD 0 must clear FD_CLOEXEC"); + Require(fcntl_nid_postfix(plain, Duplicate2DescriptorCloseOnExec, pipeDescriptors[1]) == pipeDescriptors[1], "F_DUP2FD_CLOEXEC onto a pipe must return the target"); + Require(fcntl_nid_postfix(pipeDescriptors[1], GetDescriptorFlags) == CloseOnExec, "F_DUP2FD_CLOEXEC onto a pipe must set FD_CLOEXEC"); + Require(close_nid_postfix(pipeDescriptors[0]) == 0 && close_nid_postfix(pipeDescriptors[1]) == 0, "close failed"); + const int marked = socket_nid_postfix(2, Stream | SocketCloseOnExec, 0); Require(marked >= 0 && fcntl_nid_postfix(marked, GetDescriptorFlags) == CloseOnExec, "SOCK_CLOEXEC must set FD_CLOEXEC"); Require(close_nid_postfix(marked) == 0, "close failed"); diff --git a/core/libs/tests/GuestSocketDescriptors.cpp b/core/libs/tests/GuestSocketDescriptors.cpp index 632f528ccf..9a0ac4f5e6 100644 --- a/core/libs/tests/GuestSocketDescriptors.cpp +++ b/core/libs/tests/GuestSocketDescriptors.cpp @@ -42,6 +42,8 @@ int APS5_VABI close_nid_postfix(int); int APS5_VABI sceKernelClose(int); std::int64_t APS5_VABI fstat_nid_disambig1_nid_postfix(int, FileStat*); int APS5_VABI dup_nid_postfix(int); +int APS5_VABI dup2_nid_postfix(int, int); +int APS5_VABI fcntl_nid_postfix(int, int, ...); int APS5_VABI open_nid_postfix(const char*, int, int); std::int64_t APS5_VABI read_nid_postfix(int, void*, std::uint64_t); int APS5_VABI select_nid_postfix(int, void*, void*, void*, const void*); @@ -58,6 +60,8 @@ static void Check(bool value, int line) { #define Require(value) Check((value), __LINE__) constexpr int DescriptorSetSize = 1024; +constexpr int DuplicateCommand = 0; +constexpr int DuplicateCloseOnExec = 17; constexpr int Inet = 2; constexpr int Datagram = 2; constexpr short In = 0x1; @@ -181,6 +185,29 @@ static void DuplicateKeepsSocket() { Require(close_nid_postfix(sender) == 0); } +static void SocketNumbersStayInDescriptorSet() { + const int socket = Socket(); + std::vector files; + for (;;) { + const int file = open_nid_postfix("/dev/urandom", 0, 0); + Require(file >= 0); + if (file >= DescriptorSetSize) { + Require(close_nid_postfix(file) == 0); + break; + } + files.push_back(file); + } + Require(dup_nid_postfix(socket) == -1 && *__error_nid_postfix() == 24); + Require(fcntl_nid_postfix(socket, DuplicateCommand, 0) == -1 && *__error_nid_postfix() == 24); + Require(fcntl_nid_postfix(socket, DuplicateCommand, DescriptorSetSize) == -1 && *__error_nid_postfix() == 24); + Require(fcntl_nid_postfix(socket, DuplicateCloseOnExec, 0) == -1 && *__error_nid_postfix() == 24); + Require(fcntl_nid_postfix(socket, DuplicateCloseOnExec, DescriptorSetSize) == -1 && *__error_nid_postfix() == 24); + Require(socket_nid_postfix(Inet, Datagram, 0) == -1 && *__error_nid_postfix() == 24); + Require(dup2_nid_postfix(socket, DescriptorSetSize) == -1 && *__error_nid_postfix() == 24); + for (const int file : files) Require(close_nid_postfix(file) == 0); + Require(close_nid_postfix(socket) == 0); +} + static void SocketTypeErrors() { constexpr int PrototypeError = 41; constexpr int ProtocolError = 43; @@ -202,5 +229,6 @@ int main() { SelectSocketAndFile(); CloseReleasesNumber(); DuplicateKeepsSocket(); + SocketNumbersStayInDescriptorSet(); return 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 15dabd1ef6..9a40c92040 100644 --- a/core/libs/tests/GuestSocketPair.cpp +++ b/core/libs/tests/GuestSocketPair.cpp @@ -41,6 +41,9 @@ int APS5_VABI poll_nid_postfix(PollDescriptor*, std::uint32_t, int); int APS5_VABI close_nid_postfix(int); std::int64_t APS5_VABI fstat_nid_disambig1_nid_postfix(int, FileStat*); int APS5_VABI sceKernelFstat(int, FileStat*); +int APS5_VABI sceKernelMapFlexibleMemory(void**, std::size_t, int, int); +int APS5_VABI sceKernelMprotect(const void*, std::size_t, int); +int APS5_VABI sceKernelMunmap(void*, std::size_t); int* APS5_VABI __error_nid_postfix(); } @@ -90,7 +93,83 @@ static bool Unnamed(int descriptor, int (APS5_VABI *query)(int, void*, std::uint return query(descriptor, name, &length) == 0 && length == 4 && name[0] == 16 && name[1] == 1 && name[4] == 0xaa; } -int main() { +static bool ReceiveFaults(int descriptor, Msghdr* message) { + *__error_nid_postfix() = 0; + return recvmsg_nid_postfix(descriptor, message, 0) == -1 && *__error_nid_postfix() == 14; +} + +static bool Queue(int descriptor, int receiver, const char* text) { + const auto length = static_cast(std::strlen(text)); + return send_nid_postfix(descriptor, text, static_cast(length), 0) == length && Readable(receiver); +} + +static void CheckReceiveMessageFaults() { + constexpr std::size_t page = 0x4000; + void* memory = nullptr; + Require(sceKernelMapFlexibleMemory(&memory, page * 3, 3, 0) == 0 && memory != nullptr); + auto* writable = static_cast(memory); + auto* readOnly = writable + page; + auto* inaccessible = writable + 2 * page; + char data[16]{}; + Iovec good{data, sizeof(data)}; + auto* readOnlyMessage = reinterpret_cast(readOnly); + *readOnlyMessage = {nullptr, 0, &good, 1, nullptr, 0, -1}; + auto* readOnlyVector = reinterpret_cast(readOnly + 64); + *readOnlyVector = {data, sizeof(data)}; + Require(sceKernelMprotect(readOnly, page, 1) == 0 && sceKernelMprotect(inaccessible, page, 0) == 0); + int pair[2] = {-1, -1}; + Require(socketpair_nid_postfix(Unix, Datagram, 0, pair) == 0); + + Require(Queue(pair[0], pair[1], "x")); + Require(ReceiveFaults(pair[1], reinterpret_cast(0x1000))); + Require(ReceiveFaults(pair[1], reinterpret_cast(inaccessible))); + Msghdr message{nullptr, 0, reinterpret_cast(0x1000), 1, nullptr, 0, -1}; + Require(ReceiveFaults(pair[1], &message)); + message.iov = reinterpret_cast(inaccessible); + Require(ReceiveFaults(pair[1], &message)); + char peeked[4]{}; + Require(recv_nid_postfix(pair[1], peeked, sizeof(peeked), Peek) == 1 && peeked[0] == 'x'); + + Iovec vector{reinterpret_cast(0x1000), sizeof(data)}; + message = {nullptr, 0, &vector, 1, nullptr, 0, -1}; + Require(ReceiveFaults(pair[1], &message)); + vector.base = inaccessible; + Require(Queue(pair[0], pair[1], "x") && ReceiveFaults(pair[1], &message)); + vector.base = readOnly + 128; + Require(Queue(pair[0], pair[1], "x") && ReceiveFaults(pair[1], &message)); + message = {reinterpret_cast(0x1000), 16, &good, 1, nullptr, 0, -1}; + Require(Queue(pair[0], pair[1], "x") && ReceiveFaults(pair[1], &message)); + message.name = readOnly + 128; + Require(Queue(pair[0], pair[1], "x") && ReceiveFaults(pair[1], &message)); + Require(Queue(pair[0], pair[1], "x") && ReceiveFaults(pair[1], readOnlyMessage)); + Require(fcntl_nid_postfix(pair[1], 4, 4) == 0); + Require(recv_nid_postfix(pair[1], peeked, sizeof(peeked), 0) == -1 && *__error_nid_postfix() == 35); + Require(fcntl_nid_postfix(pair[1], 4, 0) == 0); + + std::uint8_t name[32]; + std::memset(name, 0xa5, sizeof(name)); + vector = {writable, 1u << 20}; + message = {name, UINT32_MAX, &vector, 1, nullptr, 7, -1}; + Require(Queue(pair[0], pair[1], "abcd") && recvmsg_nid_postfix(pair[1], &message, 0) == 4); + Require(std::memcmp(writable, "abcd", 4) == 0 && message.flags == 0 && message.controlLength == 0); + Require(message.nameLength == 16 && name[0] == 16 && name[1] == Unix && name[16] == 0xa5); + Iovec vectors[2]{{writable, 2}, {inaccessible, 16}}; + message = {nullptr, 0, vectors, 2, nullptr, 0, -1}; + Require(Queue(pair[0], pair[1], "ef") && recvmsg_nid_postfix(pair[1], &message, 0) == 2 && std::memcmp(writable, "ef", 2) == 0); + message = {readOnly + 128, 0, readOnlyVector, 1, nullptr, 0, -1}; + Require(Queue(pair[0], pair[1], "gh") && recvmsg_nid_postfix(pair[1], &message, 0) == 2); + Require(std::memcmp(data, "gh", 2) == 0 && message.nameLength == 0); + + Require(close_nid_postfix(pair[0]) == 0 && close_nid_postfix(pair[1]) == 0); + Require(sceKernelMprotect(memory, page * 3, 3) == 0 && sceKernelMunmap(memory, page * 3) == 0); +} + +int main(int argc, char** argv) { + if (argc == 2 && std::strcmp(argv[1], "recvmsg-memory") == 0) { + CheckReceiveMessageFaults(); + return 0; + } + Require(argc == 1); int pair[2] = {-1, -1}; Require(socketpair_nid_postfix(Unix, Stream, 0, pair) == 0); Require(pair[0] >= 0 && pair[0] < DescriptorSetSize && pair[1] >= 0 && pair[1] < DescriptorSetSize && pair[0] != pair[1]); @@ -113,6 +192,7 @@ int main() { 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); @@ -130,6 +210,7 @@ int main() { 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); @@ -194,6 +275,7 @@ int main() { Require(recvmsg_nid_postfix(pair[1], &message, 0) == 4 && message.flags == 0 && std::memcmp(whole, "next", 4) == 0); Require(close_nid_postfix(pair[0]) == 0 && close_nid_postfix(pair[1]) == 0); #endif + CheckReceiveMessageFaults(); Require(Fails(2, Stream, 0, 45)); Require(Fails(28, Datagram, 0, 45)); 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/GuestStandardStreams.cpp b/core/libs/tests/GuestStandardStreams.cpp index 0ba21b86b1..c37c580e6b 100644 --- a/core/libs/tests/GuestStandardStreams.cpp +++ b/core/libs/tests/GuestStandardStreams.cpp @@ -64,6 +64,8 @@ int APS5_VABI ungetc_nid_postfix(int, FileStream*); char* APS5_VABI fgets_nid_postfix(char*, int, FileStream*); int APS5_VABI feof_nid_postfix(FileStream*); int APS5_VABI ferror_nid_postfix(FileStream*); +int APS5_VABI feof_unlocked_nid_postfix(FileStream*); +int APS5_VABI ferror_unlocked_nid_postfix(FileStream*); int APS5_VABI fileno_nid_postfix(FileStream*); void APS5_VABI clearerr_nid_postfix(FileStream*); void APS5_VABI rewind_nid_postfix(FileStream*); @@ -167,6 +169,9 @@ static void CheckUnlockedStreams() { { std::ofstream seed(filename, std::ios::binary); seed.put('A'); Require(seed.good()); } FileStream readOnly(std::fopen(filename.c_str(), "rb")); Require(putc_unlocked_nid_postfix('X', &readOnly) == EOF && (readOnly.GuestState().flags & 0x40)); + Require(ferror_unlocked_nid_postfix(&readOnly) == 1 && feof_unlocked_nid_postfix(&readOnly) == 0); + clearerr_unlocked_nid_postfix(&readOnly); + Require(ferror_unlocked_nid_postfix(&readOnly) == 0); readOnly.Close(); Require(CheckFileBytes(filename, "A", "rb", false)); Require(std::filesystem::remove(filename)); @@ -174,6 +179,17 @@ static void CheckUnlockedStreams() { static void CheckUnlockedQueries() { FileStream stream(std::tmpfile()); + Require(feof_unlocked_nid_postfix(&stream) == 0 && ferror_unlocked_nid_postfix(&stream) == 0); + Require(std::fgetc(stream.GetHandle()) == EOF); + Require(feof_unlocked_nid_postfix(&stream) == 1 && ferror_unlocked_nid_postfix(&stream) == 0); + Require((stream.GuestState().flags & 0x60) == 0x20); + stream.SetError(); + Require(feof_unlocked_nid_postfix(&stream) == 1 && ferror_unlocked_nid_postfix(&stream) == 1); + clearerr_unlocked_nid_postfix(&stream); + Require(feof_unlocked_nid_postfix(&stream) == 0 && ferror_unlocked_nid_postfix(&stream) == 0); + Require(std::fgetc(stream.GetHandle()) == EOF && std::ungetc('Q', stream.GetHandle()) == 'Q'); + Require(feof_unlocked_nid_postfix(&stream) == 0 && ferror_unlocked_nid_postfix(&stream) == 0); + Require(std::fgetc(stream.GetHandle()) == 'Q'); Require(fputc_nid_postfix(0x7f, &stream) == 0x7f); std::rewind(stream.GetHandle()); Require(fgetc_nid_postfix(&stream) == 0x7f && feof_nid_postfix(&stream) == 0); @@ -239,7 +255,7 @@ static void CheckTryLock() { Require(threw); } -static void CheckUnlockedStreamLock(bool write) { +static void CheckUnlockedStreamLock(bool write, int (APS5_VABI *query)(FileStream*) = nullptr) { FileStream stream(std::tmpfile()); Require(std::fputc('A', stream.GetHandle()) == 'A'); std::rewind(stream.GetHandle()); @@ -260,7 +276,7 @@ static void CheckUnlockedStreamLock(bool write) { } std::thread caller([&] { entered = true; - result = write ? putc_unlocked_nid_postfix('B', &stream) : getc_unlocked_nid_postfix(&stream); + result = query ? query(&stream) : write ? putc_unlocked_nid_postfix('B', &stream) : getc_unlocked_nid_postfix(&stream); done = true; }); const auto enteredDeadline = std::chrono::steady_clock::now() + std::chrono::seconds(1); @@ -277,7 +293,7 @@ static void CheckUnlockedStreamLock(bool write) { release = true; owner.join(); caller.join(); - Require(completedWhileHeld && result == (write ? 'B' : 'A')); + Require(completedWhileHeld && result == (query ? 0 : write ? 'B' : 'A')); stream.Close(); } @@ -582,6 +598,26 @@ static int CheckExclusiveStandardOutput() { return 0; } +#ifdef _WIN32 +static void CheckStandardStreamDeleteSharing() { + int index = 0; + for (auto* stream : {__stdinp_nid_postfix, __stderrp_nid_postfix}) { + const std::string filename = "anyps5-standard-delete-" + std::to_string(index++) + "-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + auto* const native = stream->GetHandle(); + const int descriptor = ::_fileno(native); + Require(freopen_nid_postfix(filename.c_str(), "w+b", stream) == stream); + Require(stream->GetHandle() == native && ::_fileno(stream->GetHandle()) == descriptor); + Require(fputc_nid_postfix('A', stream) == 'A' && fflush_nid_postfix(stream) == 0); + const std::string renamed = filename + "-moved"; + std::filesystem::rename(filename, renamed); + Require(fputc_nid_postfix('B', stream) == 'B' && fflush_nid_postfix(stream) == 0); + Require(std::filesystem::remove(renamed)); + Require(fseeko_nid_postfix(stream, 0, SEEK_SET) == 0); + Require(fgetc_nid_postfix(stream) == 'A' && fgetc_nid_postfix(stream) == 'B'); + } +} +#endif + int main(int argc, char** argv) { if (argc > 1 && std::string_view(argv[1]) == "--closed-stdout") return CheckClosedStandardOutput(); if (argc > 1 && std::string_view(argv[1]) == "--exclusive-stdout") return CheckExclusiveStandardOutput(); @@ -875,7 +911,34 @@ int main(int argc, char** argv) { CheckUnlockedStreams(); CheckUnlockedStreamLock(false); CheckUnlockedStreamLock(true); + CheckUnlockedStreamLock(false, feof_unlocked_nid_postfix); + CheckUnlockedStreamLock(false, ferror_unlocked_nid_postfix); CheckTryLock(); CheckUnlockedQueries(); - return CheckBinaryModes() ? 0 : 1; + if (!CheckBinaryModes()) return 1; +#ifdef _WIN32 + CheckStandardStreamDeleteSharing(); + const std::string standardFilename = "anyps5-standard-reopen-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + Require(freopen_nid_postfix(standardFilename.c_str(), "w", __stdoutp_nid_postfix) == __stdoutp_nid_postfix); + Require(printf_nid_postfix("standard output") == 15 && fflush_nid_postfix(__stdoutp_nid_postfix) == 0); + { std::ifstream input(standardFilename, std::ios::binary); std::string contents; std::getline(input, contents); + Require(contents == "standard output"); } + const std::string movedFilename = standardFilename + "-moved"; + std::error_code renameError; + std::filesystem::rename(standardFilename, movedFilename, renameError); + Require(!renameError && !std::filesystem::exists(standardFilename) && std::filesystem::exists(movedFilename)); + Require(printf_nid_postfix(" continued") == 10 && fflush_nid_postfix(__stdoutp_nid_postfix) == 0); + { std::ifstream input(movedFilename, std::ios::binary); std::string contents; std::getline(input, contents); + Require(contents == "standard output continued"); } + Require(std::filesystem::remove(movedFilename)); + Require(!std::filesystem::exists(movedFilename)); + Require(printf_nid_postfix(" after delete") == 13 && fflush_nid_postfix(__stdoutp_nid_postfix) == 0); + const std::string exclusiveFilename = "anyps5-standard-exclusive-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + { std::ofstream out(exclusiveFilename, std::ios::binary); out << "existing"; } + Require(freopen_nid_postfix(exclusiveFilename.c_str(), "wx", __stdoutp_nid_postfix) == nullptr && *__error_nid_postfix() == 17); + Require(std::filesystem::remove(exclusiveFilename)); + Require(printf_nid_postfix("closed") == -1 && *__error_nid_postfix() == 9); + Require(std::fputs("host stdout intact\n", stdout) >= 0 && std::fflush(stdout) == 0); +#endif + return 0; } 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..ef2f2f0a69 100644 --- a/core/libs/tests/GuestStrings.cpp +++ b/core/libs/tests/GuestStrings.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -10,15 +11,19 @@ extern "C" { void APS5_VABI bcopy_nid_postfix(const void*, void*, std::size_t); char* APS5_VABI basename_nid_postfix(const char*); +char* APS5_VABI basename_r_nid_postfix(const char*, 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*); @@ -33,6 +38,8 @@ int APS5_VABI strncat_s_nid_postfix(char*, std::size_t, const char*, std::size_t int APS5_VABI memcpy_s_nid_postfix(void*, std::size_t, const void*, std::size_t); int APS5_VABI memmove_s_nid_postfix(void*, std::size_t, const void*, std::size_t); int APS5_VABI memset_s_nid_postfix(void*, std::size_t, int, std::size_t); +void* APS5_VABI memccpy_nid_postfix(void*, const void*, int, std::size_t); +void* APS5_VABI memmem_nid_postfix(const void*, std::size_t, const void*, std::size_t); char* APS5_VABI strnstr_nid_postfix(const char*, const char*, std::size_t); std::size_t APS5_VABI strxfrm_nid_postfix(char*, const char*, std::size_t); int APS5_VABI snprintf_s_nid_postfix(char*, std::size_t, const char*, ...); @@ -262,9 +269,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(); @@ -297,6 +363,15 @@ int main() { Require(std::strcmp(basename_nid_postfix("one\\two"), "one\\two") == 0); const std::string longName(1024, 'x'); Require(basename_nid_postfix(longName.c_str()) == nullptr && *__error_nid_postfix() == 63); + char baseBuffer[1024]; + Require(basename_r_nid_postfix(path, baseBuffer) == baseBuffer && std::strcmp(baseBuffer, "two") == 0); + Require(basename_r_nid_postfix(nullptr, baseBuffer) == baseBuffer && std::strcmp(baseBuffer, ".") == 0); + Require(basename_r_nid_postfix("///", baseBuffer) == baseBuffer && std::strcmp(baseBuffer, "/") == 0); + Require(basename_r_nid_postfix("plain", baseBuffer) == baseBuffer && std::strcmp(baseBuffer, "plain") == 0); + const std::string longest(1023, 'y'); + Require(basename_r_nid_postfix(("/a/" + longest).c_str(), baseBuffer) == baseBuffer && std::strlen(baseBuffer) == 1023); + *__error_nid_postfix() = 0; + Require(basename_r_nid_postfix(longName.c_str(), baseBuffer) == nullptr && *__error_nid_postfix() == 63); const unsigned char loopbackBytes[4] = {127, 0, 0, 1}; unsigned int loopback; std::memcpy(&loopback, loopbackBytes, sizeof(loopback)); @@ -450,6 +525,16 @@ int main() { Require(strcasestr_nid_postfix("", "a") == nullptr); const char highBytes[] = {static_cast(0xff), 'A', 0}; Require(strcasestr_nid_postfix(highBytes, "a") == highBytes + 1); + const char hay[] = "hello world"; + Require(memmem_nid_postfix(hay, 11, "world", 5) == hay + 6); + Require(memmem_nid_postfix(hay, 11, "world", 0) == hay); + Require(memmem_nid_postfix(hay, 5, "world", 5) == nullptr); + Require(memmem_nid_postfix(hay, 11, "xyz", 3) == nullptr); + char dest[16] = {}; + Require(memccpy_nid_postfix(dest, "abc:def", ':', 8) == dest + 4); + Require(std::memcmp(dest, "abc:", 4) == 0); + Require(memccpy_nid_postfix(dest, "abcdef", ':', 6) == nullptr); + Require(memccpy_nid_postfix(dest, "abcdef", ':', 0) == nullptr); CheckSecureCopySizeLimits(); CheckSecureStringSizeLimits(); return CheckMemcpyOverlap() ? 0 : 1; 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 a13c41c46f..c6726534a9 100644 --- a/core/libs/tests/GuestSystemConfiguration.cpp +++ b/core/libs/tests/GuestSystemConfiguration.cpp @@ -1,10 +1,12 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libkernel/KernelErrors.hpp" +#include #include #include #include #include #include +#include struct SceKernelSystemSwVersion { std::uint64_t size; char versionString[0x1C]; @@ -23,6 +25,8 @@ int APS5_VABI sysctlbyname_nid_postfix(const char*, void*, std::size_t*, const v int APS5_VABI sysctlnametomib_nid_postfix(const char*, int*, std::size_t*); int APS5_VABI sceKernelGetSystemSwVersion_nid_postfix(SceKernelSystemSwVersion*); int APS5_VABI getpid_nid_postfix(); +void* APS5_VABI mmap_nid_postfix(void*, std::size_t, int, int, int, std::int64_t); +int APS5_VABI munmap_nid_postfix(void*, std::size_t); std::uint64_t APS5_VABI sceKernelGetTscFrequency(void); struct GuestRlimitTest { std::int64_t rlim_cur; @@ -57,13 +61,6 @@ static void CheckProcessorCountSysctl() { Require(sysctl_nid_postfix(nullptr, 2, &value, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 14); Require(sysctlbyname_nid_postfix(nullptr, &value, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 14); bool threw = false; - try { - sysctlbyname_nid_postfix("kern.osreldate", &value, &length, nullptr, 0); - } catch (const std::runtime_error&) { - threw = true; - } - Require(threw); - threw = false; const int unknown[] = {6, 99}; try { sysctl_nid_postfix(unknown, 2, &value, &length, nullptr, 0); @@ -148,13 +145,109 @@ static void CheckTscFrequencySysctl() { length = sizeof(value); Require(sysctlbyname_nid_postfix("machdep.tsc_freq", &value, &length, &value, sizeof(value)) == -1 && *__error_nid_postfix() == 1); } -static void CheckMachdepCachelineSizeSysctl() { +static bool Untouched(const unsigned char* bytes, std::size_t size) { + for (std::size_t i = 0; i < size; ++i) + if (bytes[i] != 0xaa) return false; + return true; +} +template +static bool Throws(TCall call) { + try { + call(); + } catch (const std::runtime_error&) { + return true; + } + return false; +} +static void CheckUnansweredSysctl() { + static constexpr const char* names[] = { + "hw.byteorder", + "hw.cacheline_size", + "hw.machine_arch", + "hw.usermem", + "kern.clockrate", + "kern.maxfiles", + "kern.maxfilesperproc", + "kern.maxproc", + "kern.ps_strings", + "kern.sched.name", + "kern.smp.maxcpus", + "kern.stackprot", + "kern.timecounter.hardware", + "machdep.cacheline_size", + }; + unsigned char buffer[64]; + for (const char* name : names) { + std::memset(buffer, 0xaa, sizeof(buffer)); + std::size_t length = sizeof(buffer); + *__error_nid_postfix() = 13; + Require(sysctlbyname_nid_postfix(name, buffer, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 2); + Require(length == sizeof(buffer) && Untouched(buffer, sizeof(buffer))); + length = 77; + *__error_nid_postfix() = 13; + Require(sysctlbyname_nid_postfix(name, nullptr, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 2 && length == 77); + *__error_nid_postfix() = 13; + Require(sysctlbyname_nid_postfix(name, buffer, nullptr, nullptr, 0) == -1 && *__error_nid_postfix() == 2); + Require(Untouched(buffer, sizeof(buffer))); + *__error_nid_postfix() = 13; + Require(sysctlbyname_nid_postfix(name, nullptr, nullptr, nullptr, 0) == -1 && *__error_nid_postfix() == 2); + if (std::strcmp(name, "machdep.cacheline_size") == 0) continue; + length = sizeof(buffer); + Require(Throws([&] { sysctlbyname_nid_postfix(name, buffer, &length, buffer, sizeof(buffer)); })); + } + struct Numbered { + const char* name; + int mib[2]; + }; + static constexpr Numbered numbered[] = { + {"hw.byteorder", {6, 4}}, + {"hw.machine_arch", {6, 11}}, + {"kern.clockrate", {1, 12}}, + {"kern.maxfiles", {1, 7}}, + {"kern.maxfilesperproc", {1, 27}}, + {"kern.maxproc", {1, 6}}, + {"kern.ps_strings", {1, 32}}, + }; + for (const auto& entry : numbered) { + int mib[4] = {0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a}; + std::size_t size = 4; + *__error_nid_postfix() = 13; + Require(sysctlnametomib_nid_postfix(entry.name, mib, &size) == 0 && size == 2); + Require(mib[0] == entry.mib[0] && mib[1] == entry.mib[1] && mib[2] == 0x5a5a5a5a); + Require(*__error_nid_postfix() == 13); + std::memset(buffer, 0xaa, sizeof(buffer)); + std::size_t length = sizeof(buffer); + Require(sysctl_nid_postfix(mib, 2, buffer, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 2); + Require(length == sizeof(buffer) && Untouched(buffer, sizeof(buffer))); + length = 77; + *__error_nid_postfix() = 13; + Require(sysctl_nid_postfix(mib, 2, nullptr, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 2 && length == 77); + *__error_nid_postfix() = 13; + Require(sysctl_nid_postfix(mib, 2, buffer, nullptr, nullptr, 0) == -1 && *__error_nid_postfix() == 2 && Untouched(buffer, sizeof(buffer))); + *__error_nid_postfix() = 13; + Require(sysctl_nid_postfix(mib, 2, nullptr, nullptr, nullptr, 0) == -1 && *__error_nid_postfix() == 2); + length = sizeof(buffer); + Require(Throws([&] { sysctl_nid_postfix(mib, 2, buffer, &length, buffer, sizeof(buffer)); })); + } + int mib[4] = {0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a}; + std::size_t size = 0; *__error_nid_postfix() = 13; - int value = 0x5a5a5a5a; - std::size_t length = sizeof(value); - Require(sysctlbyname_nid_postfix("machdep.cacheline_size", &value, &length, nullptr, 0) == -1); - Require(*__error_nid_postfix() == 2); - Require(value == 0x5a5a5a5a); + Require(sysctlnametomib_nid_postfix("kern.maxproc", nullptr, &size) == 0 && size == 2 && *__error_nid_postfix() == 13); + size = 1; + Require(sysctlnametomib_nid_postfix("kern.maxproc", mib, &size) == -1 && *__error_nid_postfix() == 12); + Require(size == 1 && mib[0] == 1 && mib[1] == 0x5a5a5a5a); + mib[0] = 0x5a5a5a5a; + size = 0; + Require(sysctlnametomib_nid_postfix("kern.maxproc", mib, &size) == -1 && *__error_nid_postfix() == 12); + Require(size == 0 && mib[0] == 0x5a5a5a5a); + static constexpr const char* unresolved[] = {"hw.cacheline_size", "hw.usermem", "machdep.cacheline_size"}; + for (const char* name : unresolved) { + int untouched[4] = {0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a}; + size = 4; + *__error_nid_postfix() = 13; + Require(sysctlnametomib_nid_postfix(name, untouched, &size) == -1 && *__error_nid_postfix() == 2); + Require(size == 4 && untouched[0] == 0x5a5a5a5a && untouched[1] == 0x5a5a5a5a && untouched[2] == 0x5a5a5a5a); + } } static void CheckPathnameSysctl() { *__error_nid_postfix() = 13; @@ -253,8 +346,45 @@ static void CheckSystemStringNames() { Require(mib[0] == entry.mib[0] && mib[1] == entry.mib[1] && mib[2] == 0x5a5a5a5a); } } +static void CheckOsReleaseDateSysctl() { + const int mib[] = {1, 24}; + constexpr std::int32_t expected = 1104001; + *__error_nid_postfix() = 13; + std::size_t length = 0; + Require(sysctl_nid_postfix(mib, 2, nullptr, &length, nullptr, 0) == 0 && length == sizeof(expected)); + std::int32_t value = 0; + length = sizeof(value); + Require(sysctl_nid_postfix(mib, 2, &value, &length, nullptr, 0) == 0 && value == expected && length == sizeof(value)); + Require(*__error_nid_postfix() == 13); + + value = 0; + length = sizeof(value); + Require(sysctlbyname_nid_postfix("kern.osreldate", &value, &length, nullptr, 0) == 0 && value == expected && length == sizeof(value)); + Require(*__error_nid_postfix() == 13); + + int namedMib[3] = {0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a}; + std::size_t mibLength = 3; + Require(sysctlnametomib_nid_postfix("kern.osreldate", namedMib, &mibLength) == 0 && mibLength == 2); + Require(namedMib[0] == mib[0] && namedMib[1] == mib[1] && namedMib[2] == 0x5a5a5a5a); + value = 0; + length = sizeof(value); + Require(sysctl_nid_postfix(namedMib, static_cast(mibLength), &value, &length, nullptr, 0) == 0 && value == expected); + + unsigned char truncated[4] = {0xaa, 0xaa, 0xaa, 0xaa}; + length = 2; + Require(sysctlbyname_nid_postfix("kern.osreldate", truncated, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 12); + Require(length == 2 && truncated[0] == static_cast(expected) && truncated[1] == static_cast(expected >> 8) && truncated[2] == 0xaa && truncated[3] == 0xaa); + + length = sizeof(value); + Require(sysctl_nid_postfix(mib, 2, &value, &length, &value, sizeof(value)) == -1 && *__error_nid_postfix() == 1); + Require(sysctlbyname_nid_postfix("kern.osreldate", &value, &length, &value, sizeof(value)) == -1 && *__error_nid_postfix() == 1); + + const int deeper[] = {1, 24, 0}; + length = sizeof(value); + Require(sysctl_nid_postfix(deeper, 3, &value, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 20); +} 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, 24}, {1, 37}, {1, 50}}; for (const auto& leaf : leaves) { const int mib[] = {leaf[0], leaf[1], 0}; char buffer[8] = {}; @@ -265,6 +395,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); @@ -289,23 +462,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; @@ -378,6 +578,103 @@ static void CheckArandomSysctl() { Require(sysctl_nid_postfix(mib, 2, first, nullptr, nullptr, 0) == 0); Require(*__error_nid_postfix() == 13); } +struct VmRange { + std::uint64_t start; + std::uint64_t end; + std::int32_t protection; +}; +static std::vector ReadVmmap(const int* mib) { + std::size_t length = 0; + Require(sysctl_nid_postfix(mib, 4, nullptr, &length, nullptr, 0) == 0 && length != 0 && length % 8 == 0); + std::vector buffer(length * 4 / 3); + length = buffer.size(); + Require(sysctl_nid_postfix(mib, 4, buffer.data(), &length, nullptr, 0) == 0 && length != 0 && length <= buffer.size()); + std::vector ranges; + for (std::size_t at = 0; at < length;) { + std::int32_t size = 0; + std::memcpy(&size, buffer.data() + at, sizeof(size)); + Require(size > 136 && size % 8 == 0 && at + static_cast(size) <= length); + Require(std::memchr(buffer.data() + at + 136, 0, static_cast(size) - 136) != nullptr); + VmRange range{}; + std::memcpy(&range.start, buffer.data() + at + 8, sizeof(range.start)); + std::memcpy(&range.end, buffer.data() + at + 16, sizeof(range.end)); + std::memcpy(&range.protection, buffer.data() + at + 56, sizeof(range.protection)); + Require(range.start < range.end && (ranges.empty() || ranges.back().end <= range.start)); + ranges.push_back(range); + at += static_cast(size); + } + return ranges; +} +static void CheckVmmapSysctl() { + const int self = getpid_nid_postfix(); + const int mib[] = {1, 14, 32, self}; + constexpr std::size_t Bytes = 0x10000; + *__error_nid_postfix() = 13; + void* mapped = mmap_nid_postfix(nullptr, Bytes, 3, 0x1002, -1, 0); + Require(mapped != reinterpret_cast(static_cast(-1))); + const auto address = static_cast(reinterpret_cast(mapped)); + auto ranges = ReadVmmap(mib); + const auto covering = std::find_if(ranges.begin(), ranges.end(), [&](const VmRange& range) { return range.start <= address && address + Bytes <= range.end; }); + Require(covering != ranges.end() && (covering->protection & 3) == 3); + Require(munmap_nid_postfix(mapped, Bytes) == 0); + ranges = ReadVmmap(mib); + Require(std::none_of(ranges.begin(), ranges.end(), [&](const VmRange& range) { return range.start < address + Bytes && address < range.end; })); + Require(*__error_nid_postfix() == 13); + + unsigned char truncated[32]; + std::memset(truncated, 0xaa, sizeof(truncated)); + std::size_t length = 16; + Require(sysctl_nid_postfix(mib, 4, truncated, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 12); + Require(length == 16 && truncated[16] == 0xaa); + length = sizeof(truncated); + Require(sysctl_nid_postfix(mib, 4, truncated, &length, truncated, sizeof(truncated)) == -1 && *__error_nid_postfix() == 1); + const int foreign[] = {1, 14, 32, self + 1000}; + Require(sysctl_nid_postfix(foreign, 4, nullptr, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 3); + const int current[] = {1, 14, 32, -1}; + Require(sysctl_nid_postfix(current, 4, nullptr, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 3); + Require(sysctl_nid_postfix(mib, 3, nullptr, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 22); + 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; @@ -391,6 +688,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; @@ -434,6 +732,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); @@ -480,16 +789,20 @@ int main() { CheckModelSysctl(); #endif CheckTscFrequencySysctl(); - CheckMachdepCachelineSizeSysctl(); + CheckUnansweredSysctl(); CheckPathnameSysctl(); CheckSystemStringSysctl(); CheckSystemStringNames(); + CheckOsReleaseDateSysctl(); 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/GuestSystemExit.cpp b/core/libs/tests/GuestSystemExit.cpp index 93ff93b094..4cf3ad3931 100644 --- a/core/libs/tests/GuestSystemExit.cpp +++ b/core/libs/tests/GuestSystemExit.cpp @@ -108,6 +108,15 @@ void SecondaryShutdown() { AppendMarker("secondary shutdown"); } void APS5_VABI RemainingHandler(void*) { AppendMarker("remaining handler"); } void APS5_VABI ExitingHandler(void*) { exit_nid_postfix(7); } void GuestExitShutdown() { AppendMarker("shutdown"); } +void DelayedExitDrain() { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + AppendMarker("drain"); +} +void ExitFlushAfterDrain() { AppendMarker("flush"); } +void ThrowingExitDrain() { throw std::runtime_error("exit drain test failure"); } +void BlockedExitDrain() { + for (;;) std::this_thread::sleep_for(std::chrono::seconds(1)); +} void APS5_VABI CheckShutdownStillRunning(void*) { if (LibcShutdownToken_nid_postfix().stop_requested()) std::_Exit(8); AppendMarker("guest finalizer"); @@ -193,9 +202,40 @@ int DriveRelaunch() { std::puts("Guest relaunch completed"); return 0; } + +int DriveExitDrain() { + const auto self = OwnExecutable(); + const auto marker = std::filesystem::temp_directory_path() / ("anyps5-exit-drain-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".txt"); + SetVariable(RelaunchMarker, marker.string().c_str()); + Require(RunChild(self, "--exit-drain") == 7); + std::ifstream file(marker); + const std::string text{std::istreambuf_iterator(file), std::istreambuf_iterator()}; + file.close(); + std::filesystem::remove(marker); + Require(text == "drain\nflush\n"); + Require(RunChild(self, "--exit-drain-throws") == 7); + const auto started = std::chrono::steady_clock::now(); + Require(RunChild(self, "--exit-drain-blocked") == 7); + const auto elapsed = std::chrono::steady_clock::now() - started; + Require(elapsed >= std::chrono::seconds(8) && elapsed < std::chrono::seconds(12)); + std::puts("Host exit drain completed before immediate termination and stayed bounded"); + return 0; +} } int main(int argc, char** argv) { const int firstArgument = argc > 0 && std::strncmp(argv[0], "--", 2) == 0 ? 0 : 1; + if (argc > firstArgument && std::strcmp(argv[firstArgument], "--exit-drain-driver") == 0) return DriveExitDrain(); + if (argc > firstArgument && std::strncmp(argv[firstArgument], "--exit-drain", 12) == 0) { + LibcRegisterShutdown_nid_postfix(UnexpectedCleanup); + Require(std::atexit(UnexpectedCleanup) == 0); + if (std::strcmp(argv[firstArgument], "--exit-drain-throws") == 0) LibcSetExitDrain_nid_no_patch(ThrowingExitDrain); + else if (std::strcmp(argv[firstArgument], "--exit-drain-blocked") == 0) LibcSetExitDrain_nid_no_patch(BlockedExitDrain); + else { + LibcSetExitDrain_nid_no_patch(DelayedExitDrain); + LibcSetEndFlush_nid_no_patch(ExitFlushAfterDrain); + } + _Exit_nid_postfix(7); + } if (argc > firstArgument && std::strcmp(argv[firstArgument], "--requested-exit-driver") == 0) { Require(RunChild(OwnExecutable(), "--requested-exit") == 0); return 0; diff --git a/core/libs/tests/GuestSystemFont.cpp b/core/libs/tests/GuestSystemFont.cpp index ec2f807855..f7f07156cf 100644 --- a/core/libs/tests/GuestSystemFont.cpp +++ b/core/libs/tests/GuestSystemFont.cpp @@ -5,11 +5,17 @@ #include #include #include +#include #include #include #include #include #include +#ifdef _WIN32 +#include +#else +#include +#endif extern "C" { int APS5_VABI sceFontMemoryInit(FontMemory*, void*, std::uint32_t, const FontMemoryInterface*, void*, FontMemoryDestroyFunction, void*); @@ -24,7 +30,16 @@ int APS5_VABI sceFontCloseFont(FontHandle); int APS5_VABI sceFontSetScalePixel(FontHandle, float, float); int APS5_VABI sceFontGetCharGlyphMetrics(FontHandle, std::uint32_t, FontGlyphMetrics*); int APS5_VABI sceFontGetHorizontalLayout(FontHandle, FontHorizontalLayout*); +int APS5_VABI sceFontGetKerning(FontHandle, std::uint32_t, std::uint32_t, FontKerning*); +int APS5_VABI sceFontCreateRenderer(const FontMemory*, const void*, FontRenderer*); +int APS5_VABI sceFontDestroyRenderer(FontRenderer*); +int APS5_VABI sceFontBindRenderer(FontHandle, FontRenderer); +int APS5_VABI sceFontUnbindRenderer(FontHandle); +int APS5_VABI sceFontSetupRenderScalePixel(FontHandle, float, float); +void APS5_VABI sceFontRenderSurfaceInit(FontRenderSurface*, void*, int, int, int, int); +int APS5_VABI sceFontRenderCharGlyphImageHorizontal(FontHandle, std::uint32_t, FontRenderSurface*, float, float, FontGlyphMetrics*, FontRenderOutput*); const void* APS5_VABI sceFontSelectLibraryFt(int); +const void* APS5_VABI sceFontSelectRendererFt(int); } static void Check(bool value, int line) { @@ -118,12 +133,70 @@ static void SetFontDirectory(const std::filesystem::path& directory) { #endif } +template +static std::string CaptureStderr(const std::filesystem::path& path, TRun run) { + std::fflush(stderr); +#ifdef _WIN32 + const int saved = _dup(2); +#else + const int saved = dup(2); +#endif + std::FILE* file = std::fopen(path.string().c_str(), "wb"); + Require(saved >= 0 && file != nullptr); +#ifdef _WIN32 + Require(_dup2(_fileno(file), 2) == 0); +#else + Require(dup2(fileno(file), 2) == 2); +#endif + run(); + std::fflush(stderr); +#ifdef _WIN32 + Require(_dup2(saved, 2) == 0); + _close(saved); +#else + Require(dup2(saved, 2) == 2); + close(saved); +#endif + std::fclose(file); + std::ifstream input(path, std::ios::binary); + return std::string(std::istreambuf_iterator(input), std::istreambuf_iterator()); +} + +static std::size_t Occurrences(const std::string& text, const std::string& pattern) { + std::size_t count = 0; + for (auto at = text.find(pattern); at != std::string::npos; at = text.find(pattern, at + pattern.size())) ++count; + return count; +} + static float AdvanceOf(FontHandle font, std::uint32_t code) { FontGlyphMetrics metrics{}; Require(sceFontGetCharGlyphMetrics(font, code, &metrics) == SCE_FONT_OK); return metrics.Horizontal.advance; } +static float SetAdvanceOf(FontLibrary library, std::uint32_t fontSetType, std::uint32_t code) { + FontHandle set = nullptr; + Require(sceFontOpenFontSet(library, fontSetType, 1, nullptr, &set) == SCE_FONT_OK); + Require(sceFontSetScalePixel(set, 100.0f, 100.0f) == SCE_FONT_OK); + const float advance = AdvanceOf(set, code); + Require(sceFontCloseFont(set) == SCE_FONT_OK); + return advance; +} + +static int RenderedPixels(FontHandle font, std::uint32_t code, float& advance) { + std::vector pixels(128 * 128); + FontRenderSurface surface{}; + sceFontRenderSurfaceInit(&surface, pixels.data(), 128, 1, 128, 128); + FontGlyphMetrics metrics{}; + FontRenderOutput result{}; + const int rc = sceFontRenderCharGlyphImageHorizontal(font, code, &surface, 10.0f, 100.0f, &metrics, &result); + advance = metrics.Horizontal.advance; + if (rc != SCE_FONT_OK) return rc; + int lit = 0; + for (const std::uint8_t pixel : pixels) lit += pixel != 0 ? 1 : 0; + return lit; +} + int main() { constexpr std::uint32_t EuropeanLight = 0x180700C3u; constexpr std::uint32_t EuropeanBold = 0x180700C7u; @@ -155,8 +228,13 @@ int main() { FontHandle font = nullptr; SetFontDirectory(empty); - Require(sceFontOpenFontSet(library, EuropeanBold, 1, nullptr, &font) == SCE_FONT_ERROR_FONT_OPEN_FAILED && font == nullptr); - Require(sceFontOpenFontSet(library, 0x18070046u, 1, nullptr, &font) == SCE_FONT_ERROR_NO_SUPPORT_FONTSET && font == nullptr); + const std::string warnings = CaptureStderr(root / "stderr.txt", [&] { + Require(sceFontOpenFontSet(library, EuropeanBold, 1, nullptr, &font) == SCE_FONT_ERROR_FONT_OPEN_FAILED && font == nullptr); + Require(sceFontOpenFontSet(library, EuropeanLight, 1, nullptr, &font) == SCE_FONT_ERROR_FONT_OPEN_FAILED && font == nullptr); + Require(sceFontOpenFontSet(library, 0x18070046u, 1, nullptr, &font) == SCE_FONT_ERROR_NO_SUPPORT_FONTSET && font == nullptr); + }); + Require(Occurrences(warnings, "[libSceFont] no file for system font set") == 1); + Require(Occurrences(warnings, "0x180700C7 in " + empty.string()) == 1 && Occurrences(warnings, "ANYPS5_SYSTEM_FONTS") == 1); SetFontDirectory(root / "missing"); bool threw = false; try { @@ -218,7 +296,52 @@ int main() { Require(AdvanceOf(memoryFont, 'A') == 90.0f); Require(sceFontGetHorizontalLayout(memoryFont, &layout) == SCE_FONT_OK); - for (FontHandle handle : {font, instance, substitute, memoryFont}) Require(sceFontCloseFont(handle) == SCE_FONT_OK); + constexpr std::uint32_t EuropeanJpAr = 0x180704C4u; + constexpr std::uint32_t EuropeanJpCjk = 0x1807A444u; + constexpr std::uint32_t EuropeanGbCjk = 0x1807AC44u; + constexpr std::uint32_t European = 0x18070044u; + const auto fallbackFonts = root / "fallback-fonts"; + std::filesystem::create_directories(fallbackFonts); + WriteFile(fallbackFonts / "SST-Roman.otf", SquareGlyphFont('A', 600)); + WriteFile(fallbackFonts / "SSTJpPro-Regular.otf", SquareGlyphFont(0x4E00, 630)); + WriteFile(fallbackFonts / "DFHEI5-SONY.ttf", SquareGlyphFont(0x4E00, 640)); + WriteFile(fallbackFonts / "SCEPS4Yoongd-Medium.otf", SquareGlyphFont(0xAC00, 650)); + WriteFile(fallbackFonts / "NotoSansArabic-Regular.ttf", SquareGlyphFont(0x0627, 660)); + SetFontDirectory(fallbackFonts); + Require(SetAdvanceOf(library, EuropeanJpAr, 'A') == 60.0f); + Require(SetAdvanceOf(library, EuropeanJpAr, 0x4E00) == 63.0f); + Require(SetAdvanceOf(library, EuropeanJpAr, 0x0627) == 66.0f); + Require(SetAdvanceOf(library, EuropeanJpCjk, 0x4E00) == 63.0f); + Require(SetAdvanceOf(library, EuropeanJpCjk, 0xAC00) == 65.0f); + Require(SetAdvanceOf(library, EuropeanGbCjk, 0x4E00) == 64.0f); + WriteFile(fallbackFonts / "NotoSans-Regular.ttf", SquareGlyphFont(0x1EA0, 670)); + Require(SetAdvanceOf(library, 0x18070454u, 0x1EA0) == 67.0f); + FontHandle plain = nullptr; + Require(sceFontOpenFontSet(library, European, 1, nullptr, &plain) == SCE_FONT_OK); + Require(sceFontGetCharGlyphMetrics(plain, 0x4E00, &metrics) == SCE_FONT_ERROR_NO_SUPPORT_GLYPH); + Require(sceFontCloseFont(plain) == SCE_FONT_OK); + + FontHandle mixed = nullptr; + Require(sceFontOpenFontSet(library, EuropeanJpAr, 1, nullptr, &mixed) == SCE_FONT_OK); + FontHandle mixedInstance = nullptr; + Require(sceFontOpenFontInstance(mixed, nullptr, &mixedInstance) == SCE_FONT_OK); + Require(sceFontSetScalePixel(mixedInstance, 100.0f, 100.0f) == SCE_FONT_OK); + Require(AdvanceOf(mixedInstance, 0x0627) == 66.0f); + FontKerning kerning{1.0f, 1.0f, 1.0f, 1.0f}; + Require(sceFontGetKerning(mixed, 'A', 0x4E00, &kerning) == SCE_FONT_OK && kerning.offsetX == 0.0f); + FontRenderer renderer = nullptr; + Require(sceFontCreateRenderer(&memory, sceFontSelectRendererFt(0), &renderer) == SCE_FONT_OK); + Require(sceFontBindRenderer(mixed, renderer) == SCE_FONT_OK); + Require(sceFontSetupRenderScalePixel(mixed, 100.0f, 100.0f) == SCE_FONT_OK); + float advance = 0.0f; + Require(RenderedPixels(mixed, 'A', advance) > 0 && advance == 60.0f); + Require(RenderedPixels(mixed, 0x4E00, advance) > 0 && advance == 63.0f); + Require(RenderedPixels(mixed, 0x0627, advance) > 0 && advance == 66.0f); + Require(RenderedPixels(mixed, 0xAC00, advance) == SCE_FONT_ERROR_NO_SUPPORT_GLYPH); + Require(sceFontUnbindRenderer(mixed) == SCE_FONT_OK); + Require(sceFontDestroyRenderer(&renderer) == SCE_FONT_OK); + + for (FontHandle handle : {font, instance, substitute, memoryFont, mixed, mixedInstance}) Require(sceFontCloseFont(handle) == SCE_FONT_OK); Require(sceFontDestroyLibrary(&library) == SCE_FONT_OK); Require(allocations == 0); std::filesystem::remove_all(root); diff --git a/core/libs/tests/GuestSystemGesture.cpp b/core/libs/tests/GuestSystemGesture.cpp index 426560081b..01d5123939 100644 --- a/core/libs/tests/GuestSystemGesture.cpp +++ b/core/libs/tests/GuestSystemGesture.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include extern "C" { @@ -15,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*); @@ -25,6 +27,9 @@ int APS5_VABI sceSystemGestureUpdateTouchRecognizerRectangle(std::int32_t, void* int APS5_VABI sceSystemGestureResetPrimitiveTouchRecognizer(std::int32_t); int APS5_VABI sceSystemGestureUpdateAllTouchRecognizer(std::int32_t); int APS5_VABI sceSystemGestureUpdatePrimitiveTouchRecognizer(std::int32_t, const void*); +int APS5_VABI sceSystemGestureInitializePrimitiveTouchRecognizer(const void*); +int APS5_VABI sceSystemGestureFinalizePrimitiveTouchRecognizer(void); +int APS5_VABI sceSystemGestureGetPrimitiveTouchEventByPrimitiveID(std::int32_t, std::uint16_t, void*); } static void Require(bool value) { if (!value) std::abort(); } @@ -79,6 +84,130 @@ static void TestTouchRecognizerInformation(std::int32_t handle, int invalidArgum Require(SameRectangle(information.rectangle, area)); } +static PadData TouchReport(std::uint64_t timestamp, std::initializer_list> touches) { + PadData report{}; + report.timestamp = timestamp; + for (const auto& touch : touches) { + if (report.touch_data_touch_num == 0) { + report.touch_data_touch0_id = static_cast(touch[0]); + report.touch_data_touch0_x = touch[1]; + report.touch_data_touch0_y = touch[2]; + } else { + report.touch_data_touch1_id = static_cast(touch[0]); + report.touch_data_touch1_x = touch[1]; + report.touch_data_touch1_y = touch[2]; + } + ++report.touch_data_touch_num; + } + return report; +} + +static int UpdateWithReports(std::int32_t handle, const PadData* reports, std::int32_t count) { + SystemGestureTouchPadData input{}; + input.pad_handle = 1; + input.report_number = count; + input.pad_data_buffer = reports; + return sceSystemGestureUpdatePrimitiveTouchRecognizer(handle, &input); +} + +static SystemGesturePrimitiveTouchEvent SingleEvent(std::int32_t handle) { + SystemGesturePrimitiveTouchEvent event; + std::memset(&event, 0x5a, sizeof(event)); + Require(sceSystemGestureGetPrimitiveTouchEventByIndex(handle, 0, &event) == 0); + return event; +} + +static void TestPrimitiveTouchEvents(std::int32_t handle, int invalidArgument, int invalidHandle, int indexOutOfArray) { + Require(sceSystemGestureInitializePrimitiveTouchRecognizer(nullptr) == 0); + + const PadData pressed = TouchReport(1000, {{7, 480, 471}}); + Require(UpdateWithReports(handle, &pressed, 1) == 0); + Require(sceSystemGestureGetPrimitiveTouchEventsCount(handle) == 1); + SystemGesturePrimitiveTouchEvent event = SingleEvent(handle); + Require(event.event_state == 1); + Require(event.primitive_id == 7); + Require(event.is_updated == 1); + Require(event.pressed_position.x == 480.0f && event.pressed_position.y == 471.0f); + Require(event.current_position.x == 480.0f && event.current_position.y == 471.0f); + Require(event.delta_vector.x == 0.0f && event.delta_vector.y == 0.0f); + Require(event.delta_time == 0 && event.elapsed_time == 0); + + const PadData moved = TouchReport(2000, {{7, 500, 480}}); + Require(UpdateWithReports(handle, &moved, 1) == 0); + event = SingleEvent(handle); + Require(event.event_state == 2); + Require(event.is_updated == 1); + Require(event.pressed_position.x == 480.0f && event.pressed_position.y == 471.0f); + Require(event.current_position.x == 500.0f && event.current_position.y == 480.0f); + Require(event.delta_vector.x == 20.0f && event.delta_vector.y == 9.0f); + Require(event.delta_time == 1000 && event.elapsed_time == 1000); + + const PadData held = TouchReport(3000, {{7, 500, 480}}); + Require(UpdateWithReports(handle, &held, 1) == 0); + event = SingleEvent(handle); + Require(event.event_state == 2); + Require(event.is_updated == 0); + Require(event.delta_vector.x == 0.0f && event.delta_vector.y == 0.0f); + Require(event.delta_time == 1000 && event.elapsed_time == 2000); + + const PadData released = TouchReport(4000, {}); + Require(UpdateWithReports(handle, &released, 1) == 0); + Require(sceSystemGestureGetPrimitiveTouchEventsCount(handle) == 1); + event = SingleEvent(handle); + Require(event.event_state == 3); + Require(event.is_updated == 1); + Require(event.current_position.x == 500.0f && event.current_position.y == 480.0f); + Require(event.delta_time == 1000 && event.elapsed_time == 3000); + + const PadData empty = TouchReport(5000, {}); + Require(UpdateWithReports(handle, &empty, 1) == 0); + Require(sceSystemGestureGetPrimitiveTouchEventsCount(handle) == 0); + Require(sceSystemGestureGetPrimitiveTouchEventByIndex(handle, 0, &event) == indexOutOfArray); + + const PadData pair = TouchReport(6000, {{3, 100, 200}, {4, 300, 400}}); + Require(UpdateWithReports(handle, &pair, 1) == 0); + Require(sceSystemGestureGetPrimitiveTouchEventsCount(handle) == 2); + + SystemGesturePrimitiveTouchEvent byId{}; + Require(sceSystemGestureGetPrimitiveTouchEventByPrimitiveID(handle, 4, &byId) == 0); + Require(byId.primitive_id == 4); + Require(byId.current_position.x == 300.0f && byId.current_position.y == 400.0f); + Require(sceSystemGestureGetPrimitiveTouchEventByPrimitiveID(handle, 4, nullptr) == invalidArgument); + Require(sceSystemGestureGetPrimitiveTouchEventByPrimitiveID(handle + 1, 4, &byId) == invalidHandle); + Require(Throws([&] { sceSystemGestureGetPrimitiveTouchEventByPrimitiveID(handle, 9, &byId); })); + + std::array events{}; + std::uint32_t reported = 7; + Require(sceSystemGestureGetPrimitiveTouchEvents(handle, events.data(), 1, &reported) == 0); + Require(reported == 1); + Require(events[0].primitive_id == 3); + reported = 7; + Require(sceSystemGestureGetPrimitiveTouchEvents(handle, events.data(), 2, &reported) == 0); + Require(reported == 2 && events[1].primitive_id == 4); + Require(sceSystemGestureGetPrimitiveTouchEvents(handle, nullptr, 2, &reported) == invalidArgument); + Require(sceSystemGestureGetPrimitiveTouchEventByIndex(handle, 2, &event) == indexOutOfArray); + + Require(UpdateWithReports(handle + 1, &pair, 1) == invalidHandle); + Require(sceSystemGestureUpdatePrimitiveTouchRecognizer(handle, nullptr) == 0); + Require(sceSystemGestureGetPrimitiveTouchEventsCount(handle) == 2); + + const std::array sequence{TouchReport(7000, {{3, 110, 200}}), TouchReport(8000, {{3, 130, 210}})}; + Require(UpdateWithReports(handle, sequence.data(), 2) == 0); + Require(sceSystemGestureGetPrimitiveTouchEventsCount(handle) == 1); + event = SingleEvent(handle); + Require(event.primitive_id == 3); + Require(event.current_position.x == 130.0f && event.current_position.y == 210.0f); + Require(event.delta_vector.x == 20.0f && event.delta_vector.y == 10.0f); + + Require(sceSystemGestureResetPrimitiveTouchRecognizer(handle) == 0); + Require(sceSystemGestureGetPrimitiveTouchEventsCount(handle) == 0); + + Require(UpdateWithReports(handle, &pressed, 1) == 0); + Require(sceSystemGestureGetPrimitiveTouchEventsCount(handle) == 1); + Require(sceSystemGestureFinalizePrimitiveTouchRecognizer() == 0); + Require(sceSystemGestureGetPrimitiveTouchEventsCount(handle) == 0); +} + int main() { constexpr int invalidArgument = static_cast(0x80D10002); constexpr int invalidHandle = static_cast(0x80D10003); @@ -112,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); @@ -138,5 +270,6 @@ int main() { Require(sceSystemGestureUpdatePrimitiveTouchRecognizer(handle + 1, nullptr) == invalidHandle); TestTouchRecognizerInformation(handle, invalidArgument, invalidHandle); + TestPrimitiveTouchEvents(handle, invalidArgument, invalidHandle, indexOutOfArray); return 0; } diff --git a/core/libs/tests/GuestSystemKillApp.cpp b/core/libs/tests/GuestSystemKillApp.cpp index 300d6524f6..3ebfe28013 100644 --- a/core/libs/tests/GuestSystemKillApp.cpp +++ b/core/libs/tests/GuestSystemKillApp.cpp @@ -3,9 +3,12 @@ #include #include #include +#include #include extern "C" int APS5_VABI sceSystemServiceGetAppIdOfRunningBigApp(void); extern "C" int APS5_VABI sceSystemServiceKillApp(int, int, int, int); +extern "C" int APS5_VABI sceLncUtilKillApp(unsigned); +extern "C" int APS5_VABI sceLncUtilKillAppWithReason(unsigned, int); namespace { bool cleaned = false; bool finished = false; @@ -27,6 +30,12 @@ bool Rejects(int appId, int how, int reason, int coreDump, const char* expected) catch (const std::runtime_error& error) { return std::strstr(error.what(), expected) != nullptr; } return false; } +template +bool RejectsCall(TCall call, const char* expected) { + try { call(); } + catch (const std::runtime_error& error) { return std::strstr(error.what(), expected) != nullptr; } + return false; +} } int main(int argc, char** argv) { const int appId = sceSystemServiceGetAppIdOfRunningBigApp(); @@ -37,6 +46,13 @@ int main(int argc, char** argv) { sceSystemServiceKillApp(appId, -1, 0, 0); std::_Exit(4); } + if (argc > 1 && (std::strcmp(argv[1], "--lnc-kill") == 0 || std::strcmp(argv[1], "--lnc-kill-reason") == 0)) { + LibcRegisterShutdown_nid_postfix(Cleanup); + Require(std::atexit(VerifyExit) == 0); + if (std::strcmp(argv[1], "--lnc-kill") == 0) sceLncUtilKillApp(static_cast(appId)); + else sceLncUtilKillAppWithReason(static_cast(appId), 0); + std::_Exit(4); + } LibcRegisterShutdown_nid_postfix(UnexpectedShutdown); Require(std::atexit(UnexpectedShutdown) == 0); constexpr char otherApp[] = "sceSystemServiceKillApp: application other than the running title"; @@ -48,5 +64,17 @@ int main(int argc, char** argv) { Require(Rejects(appId, -2, 0, 0, otherArguments)); Require(Rejects(appId, -1, 1, 0, otherArguments)); Require(Rejects(appId, -1, 0, 1, otherArguments)); + constexpr char lncOtherApp[] = "sceLncUtilKillApp: application other than the running title"; + constexpr char reasonOtherApp[] = "sceLncUtilKillAppWithReason: application other than the running title"; + constexpr char otherReason[] = "sceLncUtilKillAppWithReason: a reason other than 0"; + const auto running = static_cast(appId); + for (const unsigned other : {running + 1u, 0u, 0xffffffffu, running | 0x80000000u}) { + Require(RejectsCall([other] { sceLncUtilKillApp(other); }, lncOtherApp)); + Require(RejectsCall([other] { sceLncUtilKillAppWithReason(other, 0); }, reasonOtherApp)); + Require(RejectsCall([other] { sceLncUtilKillAppWithReason(other, 1); }, reasonOtherApp)); + } + for (const int reason : {1, -1, 2, 0x7fffffff}) { + Require(RejectsCall([running, reason] { sceLncUtilKillAppWithReason(running, reason); }, otherReason)); + } finished = true; } diff --git a/core/libs/tests/GuestSystemService.cpp b/core/libs/tests/GuestSystemService.cpp index dfe5ea3035..113cb6aa2d 100644 --- a/core/libs/tests/GuestSystemService.cpp +++ b/core/libs/tests/GuestSystemService.cpp @@ -8,6 +8,8 @@ extern "C" int APS5_VABI sceSystemServiceGetHdrToneMapLuminance(SystemServiceHdrToneMapLuminance* luminance); extern "C" int APS5_VABI sceSystemServiceParamGetString(int paramId, char* buf, std::size_t bufSize); extern "C" int APS5_VABI sceSystemServiceLaunchApp(const char* titleId, const char* const* arguments, const void* param); +extern "C" int APS5_VABI sceSystemServiceAddLocalProcessForPsmKit(const char* path, const char* const* arguments, const void* param); +extern "C" int APS5_VABI sceSystemServiceKillLocalProcessForPsmKit(int handle); namespace { @@ -15,9 +17,10 @@ constexpr int browserUnavailable = static_cast(0x8002002Du); void Require(bool value) { if (!value) std::abort(); } -bool ParamGetStringThrows(int paramId, char* buf, std::size_t bufSize) { +template +bool Throws(TCall call) { try { - static_cast(sceSystemServiceParamGetString(paramId, buf, bufSize)); + static_cast(call()); } catch (const std::runtime_error&) { return true; } @@ -43,6 +46,7 @@ extern "C" int APS5_VABI sceSystemServiceDisableMediaPlay(void); extern "C" int APS5_VABI sceSystemServiceReenableMediaPlay(void); extern "C" int APS5_VABI sceSystemServiceLaunchWebBrowser(const char* uri, void* param); extern "C" int APS5_VABI sceSystemServiceShowControllerSettings(void); +extern "C" int APS5_VABI sceSystemServiceShowDisplaySafeAreaSettings(void); extern "C" int APS5_VABI sceSystemServiceOpenChallengeActivity(void); extern "C" int APS5_VABI sceSystemServiceGetStatus(SystemServiceStatus* status); extern "C" int APS5_VABI sceSystemServiceGetDisplaySafeAreaInfo(SystemServiceDisplaySafeAreaInfo* info); @@ -51,6 +55,28 @@ extern "C" int APS5_VABI sceSystemServiceSetNoticeScreenSkipFlag(void); extern "C" int APS5_VABI sceSystemServiceDisableNoticeScreenSkipFlagAutoSet(void); extern "C" int APS5_VABI sceSystemServiceHideSplashScreen(void); extern "C" int APS5_VABI sceSystemServiceReceiveEvent(SystemServiceEvent* event); +extern "C" void APS5_VABI sceSystemServiceInitializePlayerDialogParam(SystemServicePlayerDialogParam* param); + +namespace { + +void TestPlayerDialogParam() { + alignas(8) unsigned char buffer[0x100]; + std::memset(buffer, 0xab, sizeof(buffer)); + sceSystemServiceInitializePlayerDialogParam(reinterpret_cast(buffer)); + unsigned char expected[0x40] = {0x40}; + std::memset(expected + 0xc, 0xff, 4); + Require(std::memcmp(buffer, expected, sizeof(expected)) == 0); + for (std::size_t i = sizeof(expected); i < sizeof(buffer); ++i) Require(buffer[i] == 0xab); + bool threw = false; + try { + sceSystemServiceInitializePlayerDialogParam(nullptr); + } catch (const std::invalid_argument&) { + threw = true; + } + Require(threw); +} + +} int main() { Require(sceSystemServicePowerTick() == SYSTEM_SERVICE_OK); @@ -67,6 +93,7 @@ int main() { Require(sceSystemServiceReenableMediaPlay() == SYSTEM_SERVICE_OK); Require(sceSystemServiceReenableMediaPlay() == SYSTEM_SERVICE_OK); Require(sceSystemServiceShowControllerSettings() == SYSTEM_SERVICE_OK); + Require(sceSystemServiceShowDisplaySafeAreaSettings() == SYSTEM_SERVICE_OK); Require(sceSystemServiceOpenChallengeActivity() == SYSTEM_SERVICE_OK); Require(sceSystemServiceLaunchWebBrowser("http://127.0.0.1:8780/video?v=0", nullptr) == browserUnavailable); unsigned char browserParam[64]; @@ -83,6 +110,13 @@ int main() { Require(LaunchAppThrows("PPSA99110", launchArguments, launchParam)); for (unsigned char byte : launchParam) Require(byte == 0x5a); Require(std::strcmp(launchArguments[0], "--backup") == 0 && launchArguments[1] == nullptr); + const char* childArguments[] = {"/app0/child.bin", nullptr}; + unsigned char processParam[0x90]; + std::memset(processParam, 0x5a, sizeof(processParam)); + Require(Throws([&] { return sceSystemServiceAddLocalProcessForPsmKit("/app0/child.bin", childArguments, processParam); })); + for (unsigned char byte : processParam) Require(byte == 0x5a); + Require(Throws([] { return sceSystemServiceAddLocalProcessForPsmKit(nullptr, nullptr, nullptr); })); + Require(Throws([] { return sceSystemServiceKillLocalProcessForPsmKit(1); })); Require(sceSystemServiceGetHdrToneMapLuminance(nullptr) == SYSTEM_SERVICE_ERROR_PARAMETER); SystemServiceHdrToneMapLuminance luminance{-1.0f, -1.0f, -1.0f}; Require(sceSystemServiceGetHdrToneMapLuminance(&luminance) == SYSTEM_SERVICE_OK); @@ -90,15 +124,27 @@ int main() { Require(luminance.max_tone_map_luminance == 100.0f); Require(luminance.min_tone_map_luminance == 0.0f); - char name[SYSTEM_SERVICE_MAX_SYSTEM_NAME_LENGTH]; + char name[SYSTEM_SERVICE_MAX_SYSTEM_NAME_LENGTH * 2]; + const auto untouched = [&name] { + for (const char byte : name) { + if (byte != 'x') return false; + } + return true; + }; std::memset(name, 'x', sizeof(name)); - Require(sceSystemServiceParamGetString(SYSTEM_SERVICE_PARAM_ID_SYSTEM_NAME, nullptr, sizeof(name)) == SYSTEM_SERVICE_ERROR_PARAMETER); - Require(sceSystemServiceParamGetString(SYSTEM_SERVICE_PARAM_ID_SYSTEM_NAME, name, 0) == SYSTEM_SERVICE_ERROR_PARAMETER); - Require(ParamGetStringThrows(SYSTEM_SERVICE_PARAM_ID_SYSTEM_NAME, name, sizeof(name) - 1)); - Require(ParamGetStringThrows(SYSTEM_SERVICE_PARAM_ID_LANG, name, sizeof(name))); - Require(name[0] == 'x'); - Require(sceSystemServiceParamGetString(SYSTEM_SERVICE_PARAM_ID_SYSTEM_NAME, name, sizeof(name)) == SYSTEM_SERVICE_OK); + Require(sceSystemServiceParamGetString(SYSTEM_SERVICE_PARAM_ID_SYSTEM_NAME, nullptr, SYSTEM_SERVICE_MAX_SYSTEM_NAME_LENGTH) == SYSTEM_SERVICE_ERROR_PARAMETER); + for (std::size_t size = 0; size < SYSTEM_SERVICE_MAX_SYSTEM_NAME_LENGTH; ++size) { + Require(sceSystemServiceParamGetString(SYSTEM_SERVICE_PARAM_ID_SYSTEM_NAME, name, size) == SYSTEM_SERVICE_ERROR_PARAMETER); + } + for (const int paramId : {-1, 0, 1, 5, 7, 40, 100, 1000, 0x7fffffff}) { + Require(sceSystemServiceParamGetString(paramId, name, sizeof(name)) == SYSTEM_SERVICE_ERROR_PARAMETER); + } + Require(sceSystemServiceParamGetString(SYSTEM_SERVICE_PARAM_ID_LANG, name, sizeof(name)) == SYSTEM_SERVICE_ERROR_PARAMETER); + Require(untouched()); + Require(sceSystemServiceParamGetString(SYSTEM_SERVICE_PARAM_ID_SYSTEM_NAME, name, SYSTEM_SERVICE_MAX_SYSTEM_NAME_LENGTH + 3) == SYSTEM_SERVICE_OK); Require(std::strcmp(name, "PS5") == 0); + for (std::size_t index = 4; index < SYSTEM_SERVICE_MAX_SYSTEM_NAME_LENGTH + 3; ++index) Require(name[index] == '\0'); + for (std::size_t index = SYSTEM_SERVICE_MAX_SYSTEM_NAME_LENGTH + 3; index < sizeof(name); ++index) Require(name[index] == 'x'); Require(sceSystemServiceGetStatus(nullptr) == SYSTEM_SERVICE_ERROR_PARAMETER); SystemServiceStatus status; @@ -122,4 +168,5 @@ int main() { SystemServiceEvent event; event.event_type = 7; Require(sceSystemServiceReceiveEvent(&event) == SYSTEM_SERVICE_ERROR_NO_EVENT); + TestPlayerDialogParam(); } diff --git a/core/libs/tests/GuestTermiosSpeed.cpp b/core/libs/tests/GuestTermiosSpeed.cpp new file mode 100644 index 0000000000..cc0a4c76b4 --- /dev/null +++ b/core/libs/tests/GuestTermiosSpeed.cpp @@ -0,0 +1,101 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#include +#include +#endif + +extern "C" int* APS5_VABI __error_nid_postfix(); + +namespace { + +using SetSpeed = int (APS5_VABI*)(void*, std::uint32_t); + +void Require(bool value, const char* message) { + if (!value) { + std::fprintf(stderr, "cfsetspeed: %s\n", message); + std::abort(); + } +} + +std::uint32_t Read(const unsigned char* data) { + std::uint32_t result; + std::memcpy(&result, data, sizeof(result)); + return result; +} + +void Check(SetSpeed setSpeed, unsigned char* attributes, std::uint32_t speed) { + std::array before; + std::memcpy(before.data(), attributes, before.size()); + *__error_nid_postfix() = 123; + Require(setSpeed(attributes, speed) == 0, "valid call failed"); + Require(*__error_nid_postfix() == 123, "successful call changed errno"); + Require(Read(attributes + 36) == speed, "incorrect input speed"); + Require(Read(attributes + 40) == speed, "incorrect output speed"); + Require(std::memcmp(before.data(), attributes, 36) == 0, "flags or control characters changed"); +} + +} + +int main(int argc, char** argv) { + Require(argc == 2, "expected the kernel library path"); +#ifdef _WIN32 + const auto library = LoadLibraryA(argv[1]); + Require(library != nullptr, "could not load libkernel"); + const auto setSpeed = reinterpret_cast(GetProcAddress(library, "cfsetspeed_nid_postfix")); + SYSTEM_INFO info{}; + GetSystemInfo(&info); + const std::size_t page = info.dwPageSize; + auto* memory = static_cast(VirtualAlloc(nullptr, 2 * page, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + Require(memory != nullptr, "could not allocate boundary fixture"); + DWORD previous = 0; + Require(VirtualProtect(memory + page, page, PAGE_NOACCESS, &previous) != 0, "could not protect boundary fixture"); +#else + const auto library = dlopen(argv[1], RTLD_NOW | RTLD_LOCAL); + Require(library != nullptr, "could not load libkernel"); + const auto setSpeed = reinterpret_cast(dlsym(library, "cfsetspeed_nid_postfix")); + const long pageSize = sysconf(_SC_PAGESIZE); + Require(pageSize > 0, "could not query page size"); + const auto page = static_cast(pageSize); + auto* memory = static_cast(mmap(nullptr, 2 * page, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); + Require(memory != MAP_FAILED, "could not allocate boundary fixture"); + Require(mprotect(memory + page, page, PROT_NONE) == 0, "could not protect boundary fixture"); +#endif + Require(setSpeed != nullptr, "export is missing"); + for (const auto speed : {0u, 1u, 50u, 9600u, 115200u, 0x12345678u, 0x80000000u, 0xffffffffu}) { + alignas(8) std::array bytes; + bytes.fill(0xa5); + Check(setSpeed, bytes.data() + 8, speed); + for (std::size_t i = 0; i < 8; ++i) { + Require(bytes[i] == 0xa5 && bytes[52 + i] == 0xa5, "write escaped the termios object"); + } + auto* boundary = memory + page - 44; + std::memset(boundary - 8, 0x5a, 52); + Check(setSpeed, boundary, speed); + for (std::size_t i = 0; i < 8; ++i) Require(boundary[-8 + static_cast(i)] == 0x5a, "write preceded the termios object"); + } + bool threw = false; + try { + static_cast(setSpeed(nullptr, 9600)); + } catch (const std::invalid_argument&) { + threw = true; + } + Require(threw, "null pointer must be rejected explicitly"); +#ifdef _WIN32 + Require(VirtualFree(memory, 0, MEM_RELEASE) != 0, "could not free boundary fixture"); + FreeLibrary(library); +#else + Require(munmap(memory, 2 * page) == 0, "could not free boundary fixture"); + dlclose(library); +#endif + return 0; +} diff --git a/core/libs/tests/GuestTimeConversion.cpp b/core/libs/tests/GuestTimeConversion.cpp index 21d59353de..7b11ebe2ef 100644 --- a/core/libs/tests/GuestTimeConversion.cpp +++ b/core/libs/tests/GuestTimeConversion.cpp @@ -10,6 +10,8 @@ #include #include #include +#include +#include #include extern "C" { @@ -24,8 +26,11 @@ std::int64_t APS5_VABI mktime_nid_postfix(GuestTm*); std::size_t APS5_VABI strftime_nid_postfix(char*, std::size_t, const char*, const GuestTm*); void APS5_VABI tzset_nid_postfix(void); extern char* tzname_nid_postfix[2]; +extern std::int64_t timezone_nid_postfix; +extern int daylight_nid_postfix; char* APS5_VABI ctime_nid_postfix(const std::int64_t*); int* APS5_VABI __error_nid_postfix(); +int APS5_VABI setenv_nid_postfix(const char*, const char*, int); double APS5_VABI libc_difftime_nid_postfix(std::int64_t, std::int64_t); double APS5_VABI difftime_nid_postfix(std::int64_t, std::int64_t); } @@ -57,7 +62,7 @@ static void CheckConcurrent(Converter convert, BufferedConverter buffered, const for (std::size_t index = 0; index < workers.size(); ++index) { workers[index] = std::thread([&, index] { start.arrive_and_wait(); - for (int iteration = 0; iteration < 50000; ++iteration) { + for (int iteration = 0; iteration < 10000; ++iteration) { const auto* result = convert(&timers[index]); if (result == nullptr || !Equal(*result, expected[index])) { matches = false; @@ -302,7 +307,7 @@ static void CheckRegionZones() { constexpr std::array zones{"America/Los_Angeles", "Europe/Berlin", "Asia/Kolkata", "America/Argentina/Buenos_Aires"}; constexpr std::array timers{-62135553600, -12609691200, -5016340800, -5000011200, -2732356800, -2209032000, -2208988800, -2208945600, -631108800}; for (const char* zone : zones) { - setenv("TZ", zone, 1); + Require(setenv_nid_postfix("TZ", zone, 1) == 0, "Could not set the region time zone"); tzset_nid_postfix(); for (const std::int64_t timer : timers) { GuestTm local{}; @@ -319,19 +324,109 @@ static void CheckRegionZones() { Require(mktime_nid_postfix(&unknown) == timer && unknown.tm_isdst == local.tm_isdst, "mktime with an unknown daylight flag did not invert localtime in a region zone"); } } - setenv("TZ", "UTC-2", 1); + Require(setenv_nid_postfix("TZ", "UTC-2", 1) == 0, "Could not restore the guest time zone"); tzset_nid_postfix(); } #endif -int main() { +static void CheckGuestZone() { + const std::int64_t moment = 172800; + GuestTm result{}; + Require(setenv_nid_postfix("TZ", "UTC-5", 1) == 0, "Could not set the guest time zone"); + Require(localtime_s_nid_postfix(&moment, &result) == &result && result.tm_hour == 5 && result.tm_gmtoff == 18000, + "localtime ignored a guest TZ change made without tzset"); + GuestTm calendar{}; + calendar.tm_year = 70; + calendar.tm_mday = 3; + calendar.tm_hour = 5; + calendar.tm_isdst = -1; + Require(mktime_nid_postfix(&calendar) == moment, "mktime ignored the guest TZ"); + Require(setenv_nid_postfix("TZ", "UTC+3", 1) == 0, "Could not change the guest time zone"); + tzset_nid_postfix(); + Require(localtime_s_nid_postfix(&moment, &result) == &result && result.tm_hour == 21 && result.tm_mday == 2 && result.tm_gmtoff == -10800, + "tzset ignored the guest TZ"); + Require(timezone_nid_postfix == 10800 && daylight_nid_postfix == 1, "tzset did not set timezone and daylight for UTC+3"); + Require(setenv_nid_postfix("TZ", "", 1) == 0, "Could not empty the guest time zone"); + tzset_nid_postfix(); + Require(localtime_s_nid_postfix(&moment, &result) == &result && result.tm_hour == 0 && result.tm_mday == 3 && result.tm_gmtoff == 0, + "An empty guest TZ was not treated as UTC"); + Require(timezone_nid_postfix == 0 && daylight_nid_postfix == 1, "tzset did not set timezone and daylight for UTC"); + Require(setenv_nid_postfix("TZ", "EST5EDT", 1) == 0, "Could not set a daylight saving guest time zone"); + tzset_nid_postfix(); + Require(timezone_nid_postfix == 18000 && daylight_nid_postfix == 1, "timezone is not the standard offset of a daylight saving zone"); + Require(setenv_nid_postfix("TZ", "UTC-2", 1) == 0, "Could not restore the guest time zone"); + tzset_nid_postfix(); + Require(setenv_nid_postfix("TZ", "EST5", 1) == 0, "Could not set the guest time zone without tzset"); + Require(localtime_s_nid_postfix(&moment, &result) == &result && result.tm_hour == 19 && result.tm_gmtoff == -18000, + "localtime ignored a guest TZ change made without tzset"); + Require(std::strcmp(tzname_nid_postfix[0], "EST") == 0, "Implicit guest TZ update did not refresh tzname"); + Require(setenv_nid_postfix("TZ", "UTC-2", 1) == 0, "Could not restore the guest time zone"); + tzset_nid_postfix(); +} + +static bool ZoneFieldsConsistent(const GuestTm& time, std::int64_t instant, int hour) { + const std::int64_t localSeconds = ((instant + time.tm_gmtoff) % 86400 + 86400) % 86400; + if (time.tm_hour * 3600 + time.tm_min * 60 + time.tm_sec != localSeconds || time.tm_hour != hour) return false; + char expected[16]; + const std::int64_t offset = time.tm_gmtoff < 0 ? -time.tm_gmtoff : time.tm_gmtoff; + std::snprintf(expected, sizeof(expected), "%02d %c%04d", hour, time.tm_gmtoff < 0 ? '-' : '+', static_cast(offset / 3600 * 100)); + char formatted[16]{}; + return strftime_nid_postfix(formatted, sizeof(formatted), "%H %z", &time) == 8 && std::strcmp(formatted, expected) == 0; +} + +static void CheckZoneRace() { + const std::int64_t moment = 172800; + std::atomic stop{false}; + std::atomic torn{0}; + std::thread flipper([&] { + for (bool west = false; !stop.load(); west = !west) setenv_nid_postfix("TZ", west ? "UTC-5" : "UTC+3", 1); + }); + std::array readers; + for (auto& reader : readers) { + reader = std::thread([&] { + for (int iteration = 0; iteration < 2500; ++iteration) { + GuestTm result{}; + if (localtime_s_nid_postfix(&moment, &result) != &result || !ZoneFieldsConsistent(result, moment, result.tm_hour)) ++torn; + GuestTm calendar{}; + calendar.tm_year = 70; + calendar.tm_mday = 3; + calendar.tm_hour = 12; + calendar.tm_isdst = -1; + const std::int64_t instant = mktime_nid_postfix(&calendar); + if (instant == -1 || calendar.tm_mday != 3 || !ZoneFieldsConsistent(calendar, instant, 12)) ++torn; + } + }); + } + for (auto& reader : readers) reader.join(); + stop = true; + flipper.join(); + Require(torn == 0, "A time conversion mixed the fields of two time zones while TZ changed"); + Require(setenv_nid_postfix("TZ", "UTC-2", 1) == 0, "Could not restore the guest time zone"); + tzset_nid_postfix(); +} + #ifdef _WIN32 - _putenv_s("TZ", "UTC-2"); - _tzset(); -#else - setenv("TZ", "UTC-2", 1); - tzset(); +static void CheckRejectedZone() { + const std::int64_t moment = 172800; + const std::string oversized = "UTC-5" + std::string(40000, 'x'); + Require(setenv_nid_postfix("TZ", oversized.c_str(), 1) == 0, "The guest environment rejected a long TZ"); + GuestTm result{}; + bool thrown = false; + try { + localtime_s_nid_postfix(&moment, &result); + } catch (const std::runtime_error&) { + thrown = true; + } + Require(thrown, "localtime accepted a TZ the host cannot hold"); + Require(setenv_nid_postfix("TZ", "UTC-2", 1) == 0, "Could not restore the guest time zone"); + tzset_nid_postfix(); + Require(localtime_s_nid_postfix(&moment, &result) == &result && result.tm_gmtoff == 7200, "localtime did not recover after a rejected TZ"); +} #endif + +int main() { + Require(setenv_nid_postfix("TZ", "UTC-2", 1) == 0, "Could not set the guest time zone"); + tzset_nid_postfix(); constexpr std::array utcCases{{ {0, 70, 0, 1, 0, 0, 0, 4, 0}, {-1, 69, 11, 31, 23, 59, 59, 3, 364}, @@ -422,22 +517,19 @@ int main() { CheckConcurrent(gmtime_nid_postfix, gmtime_s_nid_postfix, "Concurrent gmtime returned another thread's date"); CheckConcurrent(libc_localtime_nid_postfix, localtime_s_nid_postfix, "Concurrent libc_localtime returned another thread's date"); CheckConcurrent(localtime_nid_postfix, localtime_s_nid_postfix, "Concurrent localtime returned another thread's date"); + CheckGuestZone(); + CheckZoneRace(); #ifdef _WIN32 - _putenv_s("TZ", "UTC-5"); -#else - setenv("TZ", "UTC-5", 1); + CheckRejectedZone(); #endif + Require(setenv_nid_postfix("TZ", "UTC-5", 1) == 0, "Could not set the guest time zone"); tzset_nid_postfix(); GuestTm shifted{}; Require(localtime_s_nid_postfix(&epoch, &shifted) == &shifted && shifted.tm_hour == 5 && shifted.tm_gmtoff == 18000, "tzset did not pick up the new TZ"); Require(tzname_nid_postfix[0] != nullptr && std::strcmp(tzname_nid_postfix[0], "UTC") == 0 && tzname_nid_postfix[1] != nullptr, "tzname does not name the zone tzset set"); -#ifdef _WIN32 - _putenv_s("TZ", "EST5EDT"); -#else - setenv("TZ", "EST5EDT", 1); -#endif + Require(setenv_nid_postfix("TZ", "EST5EDT", 1) == 0, "Could not set the guest time zone"); tzset_nid_postfix(); Require(std::strcmp(tzname_nid_postfix[0], "EST") == 0 && std::strcmp(tzname_nid_postfix[1], "EDT") == 0, "tzname does not name the standard and daylight zones"); diff --git a/core/libs/tests/GuestTimeExtras.cpp b/core/libs/tests/GuestTimeExtras.cpp new file mode 100644 index 0000000000..6fc9d4cde0 --- /dev/null +++ b/core/libs/tests/GuestTimeExtras.cpp @@ -0,0 +1,163 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/GuestTime.hpp" +#include +#include +#include +#include +#include +#include + +struct Xtime { + std::int64_t sec; + std::int64_t nsec; +}; + +extern "C" { +GuestTm* APS5_VABI gmtime_s_nid_postfix(const std::int64_t*, GuestTm*); +GuestTm* APS5_VABI localtime_s_nid_postfix(const std::int64_t*, GuestTm*); +GuestTm* APS5_VABI gmtime_r_nid_postfix(const std::int64_t*, GuestTm*); +GuestTm* APS5_VABI localtime_r_nid_postfix(const std::int64_t*, GuestTm*); +std::int64_t APS5_VABI timegm_nid_postfix(GuestTm*); +void APS5_VABI tzset_nid_postfix(); +int APS5_VABI setenv_nid_postfix(const char*, const char*, int); +int APS5_VABI xtime_get_nid_postfix(Xtime*, int); +Xtime APS5_VABI _Xtime_to_ts_nid_postfix(const Xtime*); +Xtime APS5_VABI _Xtime_diff_to_ts_nid_postfix(const Xtime*); +} + +static void Require(bool value, const char* message) { + if (!value) { + std::fprintf(stderr, "%s\n", message); + std::abort(); + } +} + +static bool Equal(const GuestTm& left, const GuestTm& right) { + return left.tm_sec == right.tm_sec && left.tm_min == right.tm_min && left.tm_hour == right.tm_hour + && left.tm_mday == right.tm_mday && left.tm_mon == right.tm_mon && left.tm_year == right.tm_year + && left.tm_wday == right.tm_wday && left.tm_yday == right.tm_yday && left.tm_isdst == right.tm_isdst; +} + +struct TimegmCase { + int sec, min, hour, mday, mon, year; + std::int64_t expected; + int normalizedMday, normalizedMon, normalizedYear, wday, yday; +}; + +static void CheckTimegm(const TimegmCase& test) { + GuestTm time{}; + time.tm_sec = test.sec; + time.tm_min = test.min; + time.tm_hour = test.hour; + time.tm_mday = test.mday; + time.tm_mon = test.mon; + time.tm_year = test.year; + time.tm_wday = 9; + time.tm_yday = 999; + time.tm_isdst = 5; + time.tm_gmtoff = 77; + if (timegm_nid_postfix(&time) != test.expected) { + std::fprintf(stderr, "timegm(%d-%d-%d) returned the wrong time\n", test.year, test.mon, test.mday); + std::abort(); + } + Require(time.tm_mday == test.normalizedMday && time.tm_mon == test.normalizedMon && time.tm_year == test.normalizedYear + && time.tm_wday == test.wday && time.tm_yday == test.yday && time.tm_isdst == 0 && time.tm_gmtoff == 0, + "timegm did not normalize the calendar fields"); +} + +static void CheckReentrantConversions() { + constexpr std::int64_t timers[] = {0, -1, 86399, 951782400, 4107542400, 253402300799, -62135596800}; + for (const std::int64_t timer : timers) { + GuestTm expected{}; + GuestTm actual{}; + Require(gmtime_s_nid_postfix(&timer, &expected) == &expected, "gmtime_s rejected a representable time"); + Require(gmtime_r_nid_postfix(&timer, &actual) == &actual && Equal(actual, expected), "gmtime_r differs from gmtime_s"); + Require(localtime_s_nid_postfix(&timer, &expected) == &expected, "localtime_s rejected a representable time"); + Require(localtime_r_nid_postfix(&timer, &actual) == &actual && Equal(actual, expected) && actual.tm_gmtoff == 7200, + "localtime_r differs from localtime_s"); + } + const std::int64_t epoch = 0; + Require(gmtime_r_nid_postfix(&epoch, nullptr) == nullptr, "gmtime_r accepted a null result"); + Require(localtime_r_nid_postfix(&epoch, nullptr) == nullptr, "localtime_r accepted a null result"); + const std::int64_t underflow = std::numeric_limits::min(); + GuestTm scratch{}; + Require(gmtime_r_nid_postfix(&underflow, &scratch) == nullptr, "gmtime_r accepted an unrepresentable time"); +} + +static void CheckTimegmCases() { + constexpr TimegmCase cases[] = { + {0, 0, 0, 1, 0, 70, 0, 1, 0, 70, 4, 0}, + {59, 59, 23, 31, 11, 69, -1, 31, 11, 69, 3, 364}, + {7, 14, 3, 19, 9, 126, 1792379647, 19, 9, 126, 1, 291}, + {0, 0, 0, 0, 0, 70, -86400, 31, 11, 69, 3, 364}, + {0, 0, 0, 1, 12, 69, 0, 1, 0, 70, 4, 0}, + {0, 0, 0, 1, -1, 71, 28857600, 1, 11, 70, 2, 334}, + {0, 0, 0, 1, -13, 71, -2678400, 1, 11, 69, 1, 334}, + {0, 0, 0, 32, 1, 100, 952041600, 3, 2, 100, 5, 62}, + {31536000, 0, 0, 1, 0, 70, 31536000, 1, 0, 71, 5, 0}, + {0, 0, 0, 1, 2, 200, 4107542400, 1, 2, 200, 1, 59}, + {59, 59, 23, 31, 11, 8099, 253402300799, 31, 11, 8099, 5, 364}, + {0, 0, 0, 1, 0, -1899, -62135596800, 1, 0, -1899, 1, 0}, + }; + for (const auto& test : cases) CheckTimegm(test); + + GuestTm beyond{}; + beyond.tm_year = std::numeric_limits::max(); + beyond.tm_mon = 12; + beyond.tm_mday = 1; + const GuestTm before = beyond; + Require(timegm_nid_postfix(&beyond) == -1 && Equal(beyond, before), "timegm accepted a year past the int range"); + + std::mt19937_64 random(7); + for (int iteration = 0; iteration < 100000; ++iteration) { + const std::int64_t timer = static_cast(random() % 600000000000ull) - 300000000000; + GuestTm fields{}; + Require(gmtime_r_nid_postfix(&timer, &fields) == &fields, "gmtime_r rejected a representable time"); + GuestTm copy = fields; + Require(timegm_nid_postfix(©) == timer && Equal(copy, fields), "timegm did not invert gmtime_r"); + } +} + +static void CheckXtime() { + Xtime time{-5, -5}; + Require(xtime_get_nid_postfix(&time, 0) == 0 && time.sec == -5 && time.nsec == -5, "xtime_get accepted base 0"); + Require(xtime_get_nid_postfix(&time, 2) == 0 && time.sec == -5 && time.nsec == -5, "xtime_get accepted base 2"); + Require(xtime_get_nid_postfix(nullptr, 1) == 0, "xtime_get accepted a null xtime"); + const std::int64_t before = static_cast(std::time(nullptr)); + Require(xtime_get_nid_postfix(&time, 1) == 1, "xtime_get rejected TIME_UTC"); + Require(time.sec >= before - 1 && time.sec <= before + 2 && time.nsec >= 0 && time.nsec < 1000000000 && time.nsec % 1000 == 0, + "xtime_get did not return the wall clock in microseconds"); + + const Xtime unnormalized{7, -3}; + const Xtime copied = _Xtime_to_ts_nid_postfix(&unnormalized); + Require(copied.sec == 7 && copied.nsec == -3, "_Xtime_to_ts changed the time"); + + Xtime now{}; + Require(xtime_get_nid_postfix(&now, 1) == 1, "xtime_get rejected TIME_UTC"); + const Xtime past{now.sec - 1, now.nsec}; + const Xtime remaining = _Xtime_diff_to_ts_nid_postfix(&past); + Require(remaining.sec == 0 && remaining.nsec == 0, "_Xtime_diff_to_ts did not clamp a past time to zero"); + const Xtime epoch{0, 0}; + const Xtime none = _Xtime_diff_to_ts_nid_postfix(&epoch); + Require(none.sec == 0 && none.nsec == 0, "_Xtime_diff_to_ts did not clamp the epoch to zero"); + const Xtime later{now.sec + 100, now.nsec}; + const Xtime wait = _Xtime_diff_to_ts_nid_postfix(&later); + Require(wait.sec >= 98 && wait.sec <= 100 && wait.nsec >= 0 && wait.nsec < 1000000000, "_Xtime_diff_to_ts returned the wrong wait"); + const Xtime carried{now.sec, now.nsec + 5000000000}; + const Xtime carriedWait = _Xtime_diff_to_ts_nid_postfix(&carried); + Require(carriedWait.sec >= 3 && carriedWait.sec <= 5 && carriedWait.nsec >= 0 && carriedWait.nsec < 1000000000, + "_Xtime_diff_to_ts did not carry whole seconds out of nsec"); + const Xtime borrowed{now.sec + 10, -1}; + const Xtime borrowedWait = _Xtime_diff_to_ts_nid_postfix(&borrowed); + Require(borrowedWait.sec >= 8 && borrowedWait.sec <= 9 && borrowedWait.nsec >= 0 && borrowedWait.nsec < 1000000000, + "_Xtime_diff_to_ts did not borrow a second for a negative nsec"); +} + +int main() { + Require(setenv_nid_postfix("TZ", "UTC-2", 1) == 0, "Could not set the time zone"); + tzset_nid_postfix(); + CheckReentrantConversions(); + CheckTimegmCases(); + CheckXtime(); + return 0; +} diff --git a/core/libs/tests/GuestTruncateExtend.cpp b/core/libs/tests/GuestTruncateExtend.cpp new file mode 100644 index 0000000000..66dcffd80e --- /dev/null +++ b/core/libs/tests/GuestTruncateExtend.cpp @@ -0,0 +1,92 @@ +#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"); + Require(truncate_nid_postfix(FileName, -1) == -1 && *__error_nid_postfix() == 22, "truncate by path with negative length must fail with EINVAL (22)"); + Require(truncate_nid_postfix(FileName, -100) == -1 && *__error_nid_postfix() == 22, "truncate by path with negative length must fail with EINVAL (22)"); + 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/GuestUltSync.cpp b/core/libs/tests/GuestUltSync.cpp new file mode 100644 index 0000000000..9695ac0263 --- /dev/null +++ b/core/libs/tests/GuestUltSync.cpp @@ -0,0 +1,318 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceUltInitialize(); +int APS5_VABI sceUltFinalize(); +int APS5_VABI _sceUltMutexCreate(void*, const char*, void*, const void*, std::uint32_t); +int APS5_VABI sceUltMutexLock(void*); +int APS5_VABI sceUltMutexTryLock(void*); +int APS5_VABI sceUltMutexUnlock(void*); +int APS5_VABI sceUltMutexDestroy(void*); +int APS5_VABI sceUltSemaphoreOptParamInitialize(void*, std::uint32_t); +int APS5_VABI sceUltQueueOptParamInitialize(void*, std::uint32_t); +int APS5_VABI sceUltQueueDataResourcePoolOptParamInitialize(void*, std::uint32_t); +int APS5_VABI sceUltWaitingQueueResourcePoolOptParamInitialize(void*, std::uint32_t); +int APS5_VABI sceUltUlthreadOptParamInitialize(void*, std::uint32_t); +int APS5_VABI _sceUltWaitingQueueResourcePoolCreate(void*, const char*, std::uint32_t, std::uint32_t, void*, const void*, std::uint32_t); +int APS5_VABI sceUltWaitingQueueResourcePoolDestroy(void*); +int APS5_VABI sceUltReaderWriterLockOptParamInitialize(void*, std::uint32_t); +int APS5_VABI sceUltReaderWriterLockCreate(void*, const char*, void*, const void*, std::uint32_t); +int APS5_VABI sceUltReaderWriterLockDestroy(void*); +int APS5_VABI sceUltReaderWriterLockLockRead(void*); +int APS5_VABI sceUltReaderWriterLockTryLockRead(void*); +int APS5_VABI sceUltReaderWriterLockUnlockRead(void*); +int APS5_VABI sceUltReaderWriterLockLockWrite(void*); +int APS5_VABI sceUltReaderWriterLockTryLockWrite(void*); +int APS5_VABI sceUltReaderWriterLockUnlockWrite(void*); +std::uint64_t APS5_VABI sceUltUlthreadRuntimeGetWorkAreaSize(std::uint32_t, std::uint32_t); +int APS5_VABI sceUltUlthreadRuntimeCreate(void*, const char*, std::uint32_t, std::uint32_t, void*, const void*, std::uint32_t); +int APS5_VABI sceUltUlthreadRuntimeDestroy(void*); +int APS5_VABI sceUltUlthreadCreate(void*, const char*, UltUlthreadEntry, std::uint64_t, void*, std::uint64_t, void*, const void*, std::uint32_t); +int APS5_VABI sceUltUlthreadJoin(void*, std::int32_t*); +int APS5_VABI sceUltUlthreadTryJoin(void*, std::int32_t*); +int APS5_VABI sceUltUlthreadGetSelf(void**); +int APS5_VABI sceUltUlthreadYield(); +int APS5_VABI sceUltUlthreadExit(std::int32_t); +} + +namespace { + +constexpr int Ok = 0; +constexpr int Null = -2139029503; +constexpr int Invalid = -2139029500; +constexpr int State = -2139029498; +constexpr int Busy = -2139029497; + +void Require(bool value, int line) { + if (value) return; + std::fprintf(stderr, "ULT sync check failed at line %d\n", line); + std::abort(); +} + +#define CHECK(value) Require((value), __LINE__) + +struct alignas(8) Object { + std::uint8_t bytes[256]; +}; + +void MutexTryLock() { + Object mutex{}; + CHECK(sceUltMutexTryLock(nullptr) == Null); + CHECK(sceUltMutexTryLock(&mutex) == State); + CHECK(_sceUltMutexCreate(&mutex, "trylock", nullptr, nullptr, 0) == Ok); + CHECK(sceUltMutexTryLock(&mutex) == Ok); + CHECK(sceUltMutexTryLock(&mutex) == Ok); + std::atomic other{0}; + std::thread([&] { other = sceUltMutexTryLock(&mutex); }).join(); + CHECK(other == Busy); + CHECK(sceUltMutexUnlock(&mutex) == Ok); + std::thread([&] { other = sceUltMutexTryLock(&mutex); }).join(); + CHECK(other == Busy); + CHECK(sceUltMutexUnlock(&mutex) == Ok); + CHECK(sceUltMutexUnlock(&mutex) == State); + std::thread([&] { + other = sceUltMutexTryLock(&mutex); + if (other == Ok) other = sceUltMutexUnlock(&mutex); + }).join(); + CHECK(other == Ok); + CHECK(sceUltMutexLock(&mutex) == Ok); + CHECK(sceUltMutexDestroy(&mutex) == Busy); + CHECK(sceUltMutexUnlock(&mutex) == Ok); + CHECK(sceUltMutexDestroy(&mutex) == Ok); + CHECK(sceUltMutexTryLock(&mutex) == State); +} + +void OptParams() { + using Initializer = int (APS5_VABI *)(void*, std::uint32_t); + for (const Initializer initialize : {sceUltSemaphoreOptParamInitialize, sceUltQueueOptParamInitialize, sceUltQueueDataResourcePoolOptParamInitialize, + sceUltWaitingQueueResourcePoolOptParamInitialize, sceUltUlthreadOptParamInitialize, sceUltReaderWriterLockOptParamInitialize}) { + std::uint8_t bytes[136]; + std::memset(bytes, 0xa5, sizeof(bytes)); + CHECK(initialize(nullptr, 0) == Null); + CHECK(initialize(bytes, 0) == Ok); + for (std::size_t index = 0; index < 128; ++index) CHECK(bytes[index] == 0); + for (std::size_t index = 128; index < sizeof(bytes); ++index) CHECK(bytes[index] == 0xa5); + } +} + + +void WaitUntil(const std::atomic& flag) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (!flag.load() && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + CHECK(flag.load()); +} + +void Settle() { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); +} + +void ReaderWriterValidation() { + Object rwlock{}; + Object pool{}; + std::memset(&rwlock, 0x5a, sizeof(rwlock)); + CHECK(sceUltReaderWriterLockCreate(nullptr, "rwlock", nullptr, nullptr, 0) == Null); + CHECK(sceUltReaderWriterLockCreate(&rwlock, "rwlock", &pool, nullptr, 0) == Invalid); + for (const auto call : {sceUltReaderWriterLockDestroy, sceUltReaderWriterLockLockRead, sceUltReaderWriterLockTryLockRead, sceUltReaderWriterLockUnlockRead, + sceUltReaderWriterLockLockWrite, sceUltReaderWriterLockTryLockWrite, sceUltReaderWriterLockUnlockWrite}) { + CHECK(call(nullptr) == Null); + CHECK(call(&rwlock) == State); + } + CHECK(sceUltReaderWriterLockCreate(&rwlock, "rwlock", nullptr, nullptr, 0) == Ok); + for (const auto byte : rwlock.bytes) CHECK(byte == 0); + CHECK(sceUltReaderWriterLockCreate(&rwlock, "rwlock", nullptr, nullptr, 0) == State); + CHECK(sceUltReaderWriterLockUnlockRead(&rwlock) == State); + CHECK(sceUltReaderWriterLockUnlockWrite(&rwlock) == State); + CHECK(sceUltReaderWriterLockLockRead(&rwlock) == Ok); + CHECK(sceUltReaderWriterLockDestroy(&rwlock) == Busy); + CHECK(sceUltReaderWriterLockUnlockWrite(&rwlock) == State); + CHECK(sceUltReaderWriterLockUnlockRead(&rwlock) == Ok); + CHECK(sceUltReaderWriterLockLockWrite(&rwlock) == Ok); + CHECK(sceUltReaderWriterLockLockWrite(&rwlock) == State && sceUltReaderWriterLockTryLockWrite(&rwlock) == State); + CHECK(sceUltReaderWriterLockLockRead(&rwlock) == State && sceUltReaderWriterLockTryLockRead(&rwlock) == State); + CHECK(sceUltReaderWriterLockDestroy(&rwlock) == Busy); + std::atomic other{0}; + std::thread([&] { other = sceUltReaderWriterLockUnlockWrite(&rwlock); }).join(); + CHECK(other == State); + CHECK(sceUltReaderWriterLockUnlockWrite(&rwlock) == Ok); + CHECK(sceUltReaderWriterLockDestroy(&rwlock) == Ok); + CHECK(sceUltReaderWriterLockLockRead(&rwlock) == State && sceUltReaderWriterLockDestroy(&rwlock) == State); +} + +void ReaderWriterReaders() { + Object rwlock{}; + CHECK(sceUltReaderWriterLockCreate(&rwlock, "readers", nullptr, nullptr, 0) == Ok); + CHECK(sceUltReaderWriterLockLockRead(&rwlock) == Ok); + std::atomic other{0}; + std::thread([&] { + other = sceUltReaderWriterLockTryLockRead(&rwlock); + if (other == Ok) other = sceUltReaderWriterLockLockRead(&rwlock); + }).join(); + CHECK(other == Ok); + CHECK(sceUltReaderWriterLockTryLockWrite(&rwlock) == Busy); + std::atomic started{false}; + std::atomic written{false}; + std::thread writer([&] { + started = true; + other = sceUltReaderWriterLockLockWrite(&rwlock); + written = true; + if (other == Ok) other = sceUltReaderWriterLockUnlockWrite(&rwlock); + }); + WaitUntil(started); + for (int held = 3; held > 0; --held) { + Settle(); + CHECK(!written.load()); + CHECK(sceUltReaderWriterLockUnlockRead(&rwlock) == Ok); + } + writer.join(); + CHECK(written.load() && other == Ok); + CHECK(sceUltReaderWriterLockUnlockRead(&rwlock) == State); + CHECK(sceUltReaderWriterLockDestroy(&rwlock) == Ok); +} + +void ReaderWriterWriter() { + Object rwlock{}; + CHECK(sceUltReaderWriterLockCreate(&rwlock, "writer", nullptr, nullptr, 0) == Ok); + CHECK(sceUltReaderWriterLockTryLockWrite(&rwlock) == Ok); + std::atomic other{0}; + std::thread([&] { other = sceUltReaderWriterLockTryLockRead(&rwlock); }).join(); + CHECK(other == Busy); + std::thread([&] { other = sceUltReaderWriterLockTryLockWrite(&rwlock); }).join(); + CHECK(other == Busy); + std::atomic started{false}; + std::atomic read{false}; + std::thread reader([&] { + started = true; + other = sceUltReaderWriterLockLockRead(&rwlock); + read = true; + if (other == Ok) other = sceUltReaderWriterLockUnlockRead(&rwlock); + }); + WaitUntil(started); + Settle(); + CHECK(!read.load()); + CHECK(sceUltReaderWriterLockDestroy(&rwlock) == Busy); + CHECK(sceUltReaderWriterLockUnlockWrite(&rwlock) == Ok); + reader.join(); + CHECK(read.load() && other == Ok); + CHECK(sceUltReaderWriterLockTryLockWrite(&rwlock) == Ok && sceUltReaderWriterLockUnlockWrite(&rwlock) == Ok); + CHECK(sceUltReaderWriterLockDestroy(&rwlock) == Ok); +} + +void ReaderWriterPool() { + Object pool{}; + Object rwlock{}; + std::uint8_t workArea[2048]; + CHECK(_sceUltWaitingQueueResourcePoolCreate(&pool, "pool", 2, 2, workArea, nullptr, 0) == Ok); + CHECK(sceUltReaderWriterLockCreate(&rwlock, "pooled", &pool, nullptr, 0) == Ok); + CHECK(sceUltReaderWriterLockLockWrite(&rwlock) == Ok); + std::atomic started{false}; + std::atomic other{0}; + std::thread reader([&] { + started = true; + other = sceUltReaderWriterLockLockRead(&rwlock); + if (other == Ok) other = sceUltReaderWriterLockUnlockRead(&rwlock); + }); + WaitUntil(started); + Settle(); + CHECK(sceUltWaitingQueueResourcePoolDestroy(&pool) == Busy); + CHECK(sceUltReaderWriterLockUnlockWrite(&rwlock) == Ok); + reader.join(); + CHECK(other == Ok); + CHECK(sceUltReaderWriterLockDestroy(&rwlock) == Ok); + CHECK(sceUltWaitingQueueResourcePoolDestroy(&pool) == Ok); +} + +void ReaderWriterFinalize() { + Object rwlock{}; + CHECK(sceUltReaderWriterLockCreate(&rwlock, "finalize", nullptr, nullptr, 0) == Ok); + CHECK(sceUltReaderWriterLockLockWrite(&rwlock) == Ok); + std::atomic started{false}; + std::atomic other{0}; + std::thread waiter([&] { + started = true; + other = sceUltReaderWriterLockLockWrite(&rwlock); + }); + WaitUntil(started); + Settle(); + CHECK(sceUltFinalize() == Ok); + waiter.join(); + CHECK(other == State); + CHECK(sceUltReaderWriterLockUnlockWrite(&rwlock) == State); + CHECK(sceUltInitialize() == Ok); +} + +struct alignas(8) UlthreadObject { + std::uint8_t bytes[512]; +}; + +struct alignas(8) UlthreadRuntimeObject { + std::uint8_t bytes[4096]; +}; + +std::atomic gSelf{nullptr}; +std::atomic gYield{-1}; +std::atomic gPastExit{false}; + +std::int32_t APS5_VABI ExitingEntry(std::uint64_t status) { + void* self = nullptr; + if (sceUltUlthreadGetSelf(&self) == Ok) gSelf = self; + gYield = sceUltUlthreadYield(); + sceUltUlthreadExit(static_cast(status)); + gPastExit = true; + return -1; +} + +void Ulthreads() { + void* self = &gSelf; + CHECK(sceUltUlthreadGetSelf(nullptr) == Null); + CHECK(sceUltUlthreadGetSelf(&self) == State && self == &gSelf); + CHECK(sceUltUlthreadYield() == State); + CHECK(sceUltUlthreadExit(1) == State); + std::vector workArea(sceUltUlthreadRuntimeGetWorkAreaSize(2, 1)); + UlthreadRuntimeObject runtime{}; + CHECK(sceUltUlthreadRuntimeCreate(&runtime, "runtime", 2, 1, workArea.data(), nullptr, 0) == Ok); + UlthreadObject first{}; + std::uint8_t context[4096]; + CHECK(sceUltUlthreadCreate(&first, "exits", ExitingEntry, 42, context, sizeof(context), &runtime, nullptr, 0) == Ok); + std::int32_t status = 0; + CHECK(sceUltUlthreadJoin(&first, &status) == Ok); + CHECK(status == 42 && gSelf.load() == &first && gYield.load() == Ok && !gPastExit.load()); + UlthreadObject second{}; + gSelf = nullptr; + CHECK(sceUltUlthreadCreate(&second, "tryjoin", ExitingEntry, 7, context, sizeof(context), &runtime, nullptr, 0) == Ok); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + int joined = Busy; + while (joined == Busy && std::chrono::steady_clock::now() < deadline) { + joined = sceUltUlthreadTryJoin(&second, &status); + if (joined == Busy) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + CHECK(joined == Ok && status == 7 && gSelf.load() == &second); + CHECK(sceUltUlthreadRuntimeDestroy(&runtime) == Ok); +} + +} + +int main(int argc, char** argv) { + if (argc != 2) return 2; + CHECK(sceUltInitialize() == Ok); + if (std::strcmp(argv[1], "mutex-trylock") == 0) MutexTryLock(); + else if (std::strcmp(argv[1], "opt-params") == 0) OptParams(); + else if (std::strcmp(argv[1], "rwlock-validation") == 0) ReaderWriterValidation(); + else if (std::strcmp(argv[1], "rwlock-readers") == 0) ReaderWriterReaders(); + else if (std::strcmp(argv[1], "rwlock-writer") == 0) ReaderWriterWriter(); + else if (std::strcmp(argv[1], "rwlock-pool") == 0) ReaderWriterPool(); + else if (std::strcmp(argv[1], "rwlock-finalize") == 0) ReaderWriterFinalize(); + else if (std::strcmp(argv[1], "ulthreads") == 0) Ulthreads(); + else return 2; + CHECK(sceUltFinalize() == Ok); + std::puts("ULT sync tests passed"); + return 0; +} diff --git a/core/libs/tests/GuestUltThread.cpp b/core/libs/tests/GuestUltThread.cpp index b076be3735..0d074c1d20 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,8 @@ 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 sceUltGetConditionVariableInfo(); } static constexpr int Ok = 0; @@ -106,6 +109,14 @@ 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 threwInfo = false; + try { + sceUltGetConditionVariableInfo(); + } catch (const std::runtime_error&) { + threwInfo = true; + } + CHECK(threwInfo); } static void TryJoin() { diff --git a/core/libs/tests/GuestUmtxOp.cpp b/core/libs/tests/GuestUmtxOp.cpp index 56abb976c2..97eef1d837 100644 --- a/core/libs/tests/GuestUmtxOp.cpp +++ b/core/libs/tests/GuestUmtxOp.cpp @@ -20,6 +20,8 @@ static constexpr int UMTX_OP_MUTEX_LOCK = 5; static constexpr int UMTX_OP_WAIT_UINT = 11; static constexpr int UMTX_OP_WAIT_UINT_PRIVATE = 15; static constexpr int UMTX_OP_WAKE_PRIVATE = 16; +static constexpr int UMTX_OP_SEM_WAIT = 19; +static constexpr int UMTX_OP_SEM_WAKE = 20; static constexpr int UMTX_OP_NWAKE_PRIVATE = 21; static constexpr int UMTX_OP_SEM2_WAIT = 23; static constexpr int UMTX_OP_MAX = 27; @@ -270,6 +272,61 @@ static void NwakeReadsOnlyTheCountedEntries() { Require(Umtx(addresses, UMTX_OP_NWAKE_PRIVATE, 1) == 0); } +struct GuestSemaphore { + std::uint32_t hasWaiters; + std::uint32_t count; + std::uint32_t flags; +}; + +static void SemWaitReturnsAtOnceWhileTheCountIsNonzero() { + GuestSemaphore sem{0, 2, 0}; + KernelTimespec none{0, 0}; + Require(Umtx(&sem, UMTX_OP_SEM_WAIT, 0, SizeOf(sizeof(none)), &none) == 0); + Require(sem.hasWaiters == 1); + Require(sem.count == 2); +} + +static void SemWaitTimesOutWhileTheCountIsZero() { + GuestSemaphore sem{0, 0, 0}; + KernelTimespec relative{0, 20000000}; + auto start = std::chrono::steady_clock::now(); + Require(FailsWith(Umtx(&sem, UMTX_OP_SEM_WAIT, 0, SizeOf(sizeof(relative)), &relative), GUEST_ETIMEDOUT)); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(15)); + Require(sem.hasWaiters == 1); +} + +static void SemWakeReleasesTheWaiterAndClearsTheFlag() { + GuestSemaphore sem{0, 0, 0}; + std::atomic finished{0}; + int result = -2; + std::thread waiter([&] { + KernelTimespec failsafe = FailsafeTimeout(); + result = Umtx(&sem, UMTX_OP_SEM_WAIT, 0, SizeOf(sizeof(failsafe)), &failsafe); + ++finished; + }); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (std::atomic_ref(sem.hasWaiters).load() == 0) { + Require(std::chrono::steady_clock::now() < deadline); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + std::this_thread::sleep_for(SETTLE); + Require(finished.load() == 0); + + std::atomic_ref(sem.count).store(1); + Require(Umtx(&sem, UMTX_OP_SEM_WAKE, 0) == 0); + waiter.join(); + Require(result == 0); + Require(sem.hasWaiters == 0); +} + +static void SemWakeWithoutWaitersSucceeds() { + GuestSemaphore sem{0, 0, 0}; + Require(Umtx(&sem, UMTX_OP_SEM_WAKE, 0) == 0); + Require(sem.hasWaiters == 0); + Require(FailsWith(Umtx(nullptr, UMTX_OP_SEM_WAKE, 0), GUEST_EFAULT)); + Require(FailsWith(Umtx(nullptr, UMTX_OP_SEM_WAIT, 0), GUEST_EFAULT)); +} + static void RejectsBadObjectsAndOperations() { Require(FailsWith(Umtx(nullptr, UMTX_OP_WAIT, 0), GUEST_EFAULT)); Require(FailsWith(Umtx(nullptr, UMTX_OP_WAIT_UINT, 0), GUEST_EFAULT)); @@ -281,11 +338,14 @@ static void RejectsBadObjectsAndOperations() { Require(FailsWith(Umtx(&value, UMTX_OP_MAX, 0), GUEST_EINVAL)); Require(FailsWith(Umtx(&value, -1, 0), GUEST_EINVAL)); + for (const std::uint64_t owner : {std::uint64_t{0}, std::uint64_t{123456}, (std::uint64_t{1} << 63) | 123456}) { + std::uint64_t word = owner; + Require(FailsWith(Umtx(&word, 1, 123456), GUEST_EOPNOTSUPP)); + Require(FailsWith(Umtx(&word, 0, 123456), GUEST_EOPNOTSUPP)); + Require(word == owner); + } + bool threw = false; - try { Umtx(&value, 1, 0); } - catch (const std::runtime_error&) { threw = true; } - Require(threw); - threw = false; try { Umtx(&value, UMTX_OP_MUTEX_LOCK, 0); } catch (const std::runtime_error&) { threw = true; } Require(threw); @@ -305,5 +365,9 @@ int main() { WakeWithoutWaitersSucceeds(); NwakeWakesEveryWaiterOnEachListedAddress(); NwakeReadsOnlyTheCountedEntries(); + SemWaitReturnsAtOnceWhileTheCountIsNonzero(); + SemWaitTimesOutWhileTheCountIsZero(); + SemWakeReleasesTheWaiterAndClearsTheFlag(); + SemWakeWithoutWaitersSucceeds(); RejectsBadObjectsAndOperations(); } diff --git a/core/libs/tests/GuestUserServiceLifecycle.cpp b/core/libs/tests/GuestUserServiceLifecycle.cpp index 5603a52319..c169e42d26 100644 --- a/core/libs/tests/GuestUserServiceLifecycle.cpp +++ b/core/libs/tests/GuestUserServiceLifecycle.cpp @@ -2,9 +2,11 @@ #include "prx/libSceUserService/UserService.hpp" #include #include +#include extern "C" { int APS5_VABI sceUserServiceInitialize(const void* params); +int APS5_VABI sceUserServiceInitialize2(int priority); int APS5_VABI sceUserServiceTerminate(void); int APS5_VABI sceUserServiceGetLoginUserIdList(UserServiceLoginUserIdList* user_id_list); int APS5_VABI sceUserServiceGetEvent(SceUserServiceEvent* event); @@ -31,6 +33,35 @@ void RequireLoginOnce() { Require(sceUserServiceGetEvent(&event) == USER_SERVICE_ERROR_NO_EVENT); } +int InitializeWithPriority(int priority) { + return sceUserServiceInitialize(&priority); +} + +void TestThreadPriority() { + for (int priority : {0, 0xFF, 0x300, -1}) { + Require(InitializeWithPriority(priority) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + Require(sceUserServiceInitialize2(priority) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + } + RequireNotInitialized(); + + Require(sceUserServiceInitialize2(0x100) == USER_SERVICE_OK); + RequireLoginOnce(); + for (int priority : {0x100, 700, 0x2FF}) { + Require(InitializeWithPriority(priority) == USER_SERVICE_ERROR_ALREADY_INITIALIZED); + Require(sceUserServiceInitialize2(priority) == USER_SERVICE_ERROR_ALREADY_INITIALIZED); + } + for (int priority : {0, 0xFF, 0x300, -1}) { + Require(InitializeWithPriority(priority) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + Require(sceUserServiceInitialize2(priority) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + } + Require(sceUserServiceInitialize(nullptr) == USER_SERVICE_ERROR_ALREADY_INITIALIZED); + Require(sceUserServiceTerminate() == USER_SERVICE_OK); + + Require(InitializeWithPriority(0x2FF) == USER_SERVICE_OK); + RequireLoginOnce(); + Require(sceUserServiceTerminate() == USER_SERVICE_OK); +} + } int main() { @@ -51,4 +82,6 @@ int main() { Require(sceUserServiceInitialize(nullptr) == USER_SERVICE_OK); RequireLoginOnce(); Require(sceUserServiceTerminate() == USER_SERVICE_OK); + + TestThreadPriority(); } 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/GuestVideodec.cpp b/core/libs/tests/GuestVideodec.cpp new file mode 100644 index 0000000000..f4ad8e987e --- /dev/null +++ b/core/libs/tests/GuestVideodec.cpp @@ -0,0 +1,247 @@ +#include +#include +#include +#include +#include +#include "prx/libc/include/General.hpp" +#include "H264Fixture.hpp" + +struct ConfigInfo { + std::uint64_t thisSize; + std::uint32_t codecType; + std::uint32_t profile; + std::uint32_t maxLevel; + std::int32_t maxFrameWidth; + std::int32_t maxFrameHeight; + std::int32_t maxDpbFrameCount; + std::uint64_t videodecFlags; +}; + +struct ResourceInfo { + std::uint64_t thisSize; + std::uint64_t cpuMemorySize; + void* cpuMemory; + std::uint64_t cpuGpuMemorySize; + void* cpuGpuMemory; + std::uint64_t maxFrameBufferSize; + std::uint32_t frameBufferAlignment; +}; + +struct Ctrl { + std::uint64_t thisSize; + void* handle; + std::uint64_t version; +}; + +struct FrameBuffer { + std::uint64_t thisSize; + void* frameBuffer; + std::uint64_t frameBufferSize; +}; + +struct AvcInfo { + std::uint32_t numUnitsInTick; + std::uint32_t timeScale; + std::uint8_t fixedFrameRateFlag; + std::uint8_t aspectRatioIdc; + std::uint16_t sarWidth; + std::uint16_t sarHeight; + std::uint8_t colourPrimaries; + std::uint8_t transferCharacteristics; + std::uint8_t matrixCoefficients; + std::uint8_t videoFullRangeFlag; + std::uint32_t frameCropLeftOffset; + std::uint32_t frameCropRightOffset; + std::uint32_t frameCropTopOffset; + std::uint32_t frameCropBottomOffset; +}; + +union CodecInfo { + std::uint8_t reserved[64]; + AvcInfo avc; +}; + +struct PictureInfo { + std::uint64_t thisSize; + std::uint32_t isValid; + std::uint32_t codecType; + std::uint32_t frameWidth; + std::uint32_t framePitch; + std::uint32_t frameHeight; + std::uint32_t isErrorPic; + std::uint64_t ptsData; + std::uint64_t attachedData; + CodecInfo codec; +}; + +struct InputData { + std::uint64_t thisSize; + const std::uint8_t* auData; + std::uint64_t auSize; + std::uint64_t ptsData; + std::uint64_t dtsData; + std::uint64_t attachedData; +}; + +static_assert(sizeof(PictureInfo) == 112 && sizeof(AvcInfo) == 36); +static_assert(sizeof(ConfigInfo) == 40 && sizeof(ResourceInfo) == 56 && sizeof(Ctrl) == 24); + +extern "C" { +int APS5_VABI sceVideodecCreateDecoder(const ConfigInfo*, const ResourceInfo*, Ctrl*); +int APS5_VABI sceVideodecDecode(Ctrl*, const InputData*, FrameBuffer*, PictureInfo*); +int APS5_VABI sceVideodecDeleteDecoder(Ctrl*); +int APS5_VABI sceVideodecFlush(Ctrl*, FrameBuffer*, PictureInfo*); +int APS5_VABI sceVideodecReset(Ctrl*); +} + +namespace { + +using namespace H264Fixture; + +constexpr int ErrorApiFail = static_cast(0x80C10000); +constexpr int ErrorStructSize = static_cast(0x80C10002); +constexpr int ErrorHandle = static_cast(0x80C10003); +constexpr int ErrorAuSize = static_cast(0x80C10009); +constexpr int ErrorAuPointer = static_cast(0x80C1000A); +constexpr int ErrorFrameBufferSize = static_cast(0x80C1000B); +constexpr int ErrorFrameBufferPointer = static_cast(0x80C1000C); +constexpr int ErrorArgumentPointer = static_cast(0x80C1000F); + +constexpr std::uint32_t Pitch = 64; +constexpr std::uint32_t PictureHeight = 48; +constexpr std::size_t FrameBytes = static_cast(Pitch) * PictureHeight * 3 / 2; + +void check(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +struct Session { + Ctrl ctrl{sizeof(Ctrl)}; + std::vector buffer; + + Session() : buffer(FrameBytes) { + const ConfigInfo config{sizeof(ConfigInfo), 1, 100, 42, static_cast(Width), + static_cast(CodedHeight), 4, 0}; + const ResourceInfo resource{sizeof(ResourceInfo), 16u << 20u, nullptr, 16u << 20u, nullptr, + FrameBytes, 256}; + check(sceVideodecCreateDecoder(&config, &resource, &ctrl) == 0, "decoder creation failed"); + check(ctrl.handle != nullptr && ctrl.version == 1, "the control block was not filled"); + } + + ~Session() { sceVideodecDeleteDecoder(&ctrl); } + + FrameBuffer MakeTarget() { return FrameBuffer{sizeof(FrameBuffer), buffer.data(), buffer.size()}; } +}; + +void testDecode(const std::vector>& units, std::uint32_t cropTop, std::uint32_t cropBottom) { + Session session; + check(units.size() == PictureHashes.size(), "unexpected access unit count"); + std::vector hashes; + std::vector timestamps; + const auto collect = [&](PictureInfo& picture) { + if (!picture.isValid) return; + check(picture.framePitch == Pitch && picture.frameHeight == PictureHeight, "unexpected picture geometry"); + check(picture.frameWidth == Width, "unexpected picture width"); + check(picture.codecType == 0 && !picture.isErrorPic, "unexpected picture flags"); + check(picture.codec.avc.frameCropTopOffset == cropTop && picture.codec.avc.frameCropBottomOffset == cropBottom, "unexpected crop"); + hashes.push_back(hashNv12(session.buffer.data(), Pitch, PictureHeight)); + timestamps.push_back(picture.ptsData); + }; + 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}; + auto target = session.MakeTarget(); + PictureInfo picture{sizeof(PictureInfo)}; + const auto status = sceVideodecDecode(&session.ctrl, &input, &target, &picture); + check(status == 0, "decode of unit " + std::to_string(unit) + " failed with " + std::to_string(status)); + collect(picture); + } + for (;;) { + auto target = session.MakeTarget(); + PictureInfo picture{sizeof(PictureInfo)}; + check(sceVideodecFlush(&session.ctrl, &target, &picture) == 0, "flush failed"); + if (!picture.isValid) break; + collect(picture); + } + check(hashes.size() == PictureHashes.size(), "missing pictures"); + for (std::size_t index = 0; index < hashes.size(); ++index) { + check(hashes[index] == PictureHashes[index], "picture " + std::to_string(index) + " differs from the reference"); + const auto unit = DisplayOrderUnits[index]; + check(timestamps[index] == 1000 + unit, "pictures out of display order"); + } +} + +void testDecode() { + testDecode(accessUnits(false), 0, (CodedHeight - Height) / 2); +} + +void testDecodeCropped() { + const auto stream = croppedStream(); + testDecode(accessUnits(stream.data(), stream.size(), false), 4, 4); +} + +void testErrors() { + const ConfigInfo config{sizeof(ConfigInfo), 1, 100, 42, static_cast(Width), + static_cast(CodedHeight), 4, 0}; + const ResourceInfo resource{sizeof(ResourceInfo), 16u << 20u, nullptr, 16u << 20u, nullptr, FrameBytes, 256}; + Ctrl ctrl{sizeof(Ctrl)}; + check(sceVideodecCreateDecoder(nullptr, &resource, &ctrl) == ErrorArgumentPointer, "a null configuration was accepted"); + check(sceVideodecCreateDecoder(&config, &resource, nullptr) == ErrorArgumentPointer, "a null control block was accepted"); + const ConfigInfo shortConfig{sizeof(ConfigInfo) - 8}; + check(sceVideodecCreateDecoder(&shortConfig, &resource, &ctrl) == ErrorStructSize, "a short configuration was accepted"); + check(sceVideodecCreateDecoder(&config, &resource, &ctrl) == 0, "decoder creation failed"); + Ctrl foreign{sizeof(Ctrl)}; + foreign.handle = reinterpret_cast(static_cast(0x1234)); + check(sceVideodecDeleteDecoder(&foreign) == ErrorHandle, "an unknown decoder was deleted"); + check(sceVideodecReset(&foreign) == ErrorHandle, "an unknown decoder was reset"); + const InputData empty{sizeof(InputData), nullptr, 0, 0, 0, 0}; + FrameBuffer frame{sizeof(FrameBuffer), nullptr, 0}; + PictureInfo picture{sizeof(PictureInfo)}; + check(sceVideodecDecode(nullptr, &empty, &frame, &picture) == ErrorArgumentPointer, "a null control block was accepted"); + check(sceVideodecDecode(&ctrl, &empty, &frame, &picture) == ErrorAuPointer, "a null access unit was accepted"); + const InputData sized{sizeof(InputData), reinterpret_cast(&config), 0, 0, 0, 0}; + check(sceVideodecDecode(&ctrl, &sized, &frame, &picture) == ErrorAuSize, "an empty access unit was accepted"); + FrameBuffer missing{sizeof(FrameBuffer), nullptr, FrameBytes}; + const auto units = accessUnits(false); + const InputData input{sizeof(InputData), units[0].data(), units[0].size(), 0, 0, 0}; + check(sceVideodecDecode(&ctrl, &input, &missing, &picture) == ErrorFrameBufferPointer, "a null frame buffer was accepted"); + std::vector storage(static_cast(FrameBytes)); + FrameBuffer shortBuffer{sizeof(FrameBuffer), storage.data(), FrameBytes - 1}; + int refusals = 0; + for (std::size_t unit = 0; unit < units.size(); ++unit) { + const InputData unit0{sizeof(InputData), units[unit].data(), units[unit].size(), 1000 + unit, unit, 0xa0 + unit}; + const auto status = sceVideodecDecode(&ctrl, &unit0, &shortBuffer, &picture); + check(status == 0 || status == ErrorFrameBufferSize, "a decode failed"); + if (status == ErrorFrameBufferSize) ++refusals; + } + check(refusals > 0, "no picture was refused for the short frame buffer"); + check(sceVideodecDeleteDecoder(&ctrl) == 0, "the decoder was not deleted"); + check(sceVideodecDeleteDecoder(&ctrl) == ErrorHandle, "the decoder was deleted twice"); +} + +void testReset() { + Session session; + const auto units = accessUnits(false); + const InputData input{sizeof(InputData), units[0].data(), units[0].size(), 1000, 0, 0xa0}; + auto target = session.MakeTarget(); + PictureInfo picture{sizeof(PictureInfo)}; + check(sceVideodecDecode(&session.ctrl, &input, &target, &picture) == 0, "decode failed"); + check(sceVideodecReset(&session.ctrl) == 0, "reset failed"); + check(sceVideodecFlush(&session.ctrl, &target, &picture) == 0, "flush failed"); + check(!picture.isValid, "a reset decoder produced a picture"); +} + +} + +int main() { + try { + testDecode(); + testDecodeCropped(); + testErrors(); + testReset(); + std::puts("Videodec tests passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/tests/GuestVideodec2.cpp b/core/libs/tests/GuestVideodec2.cpp index 4212ef5138..101240eb6a 100644 --- a/core/libs/tests/GuestVideodec2.cpp +++ b/core/libs/tests/GuestVideodec2.cpp @@ -390,6 +390,12 @@ void testClip(std::uint32_t codec, std::uint32_t profile, const std::vector(&generic.info), nullptr) == 0, "generic VP9 picture info failed"); + const auto& same = generic.info; + check(same.thisSize == sizeof(Vp9PictureInfo) && same.isValid && same.ptsData == vp9.ptsData && same.dtsData == vp9.dtsData && same.attachedData == vp9.attachedData && same.profile == vp9.profile && same.level == vp9.level && + same.keyFrameFlag == vp9.keyFrameFlag && same.intraOnly == vp9.intraOnly && same.bitDepth == vp9.bitDepth && same.colorRange == vp9.colorRange && same.colorSpace == vp9.colorSpace && same.numFrames == vp9.numFrames && + same.frameWidth == vp9.frameWidth && same.frameHeight == vp9.frameHeight && same.renderWidth == vp9.renderWidth && same.renderHeight == vp9.renderHeight && generic.guard == 0x123456789abcdef0ull, "generic picture info differs from the VP9 layout"); } else checkThrows([&] { sceVideodec2GetVp9PictureInfo_nid_postfix(&output, &vp9); }); check(hashNv12(buffer.data(), output.framePitch, output.frameHeight) == hashes[pictures], "picture " + std::to_string(pictures) + " differs from the reference"); ++pictures; @@ -596,6 +602,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 +907,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/GuestVideodec2Unimplemented.cpp b/core/libs/tests/GuestVideodec2Unimplemented.cpp new file mode 100644 index 0000000000..6f2fc0ef0a --- /dev/null +++ b/core/libs/tests/GuestVideodec2Unimplemented.cpp @@ -0,0 +1,51 @@ +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceVideodec2TermLibrary(); +int APS5_VABI sceVideodec2QueryLibraryInfo(); +int APS5_VABI sceVideodec2QueryDecoderInfo(); +int APS5_VABI sceVideodec2MapMemory(); +int APS5_VABI sceVideodec2MapDirectMemory(void* decoder, const void* memory); +int APS5_VABI sceVideodec2PresetDecoderConfigInfo(); +int APS5_VABI sceVideodec2PresetDecoderConfigInfoAvc(); +int APS5_VABI sceVideodec2PresetDecoderConfigInfoHevc(); +int APS5_VABI sceVideodec2PresetDecoderConfigInfoVp9(); +int APS5_VABI sceVideodec2CreateDecoderBid(); +int APS5_VABI sceVideodec2CreateHevcDecoder(); +int APS5_VABI sceVideodec2QueryHevcDecoderMemoryInfo(); +int APS5_VABI sceVideodec2GetHevcPictureInfo(); +} + +static bool ReportsNotImplemented(const std::string& name, const std::function& call) { + try { + call(); + } catch (const std::runtime_error& error) { + if (error.what() == name + " not implemented") return true; + std::fprintf(stderr, "%s threw: %s\n", name.c_str(), error.what()); + return false; + } + std::fprintf(stderr, "%s returned instead of reporting that it is not implemented\n", name.c_str()); + return false; +} + +int main() { + bool passed = ReportsNotImplemented("sceVideodec2TermLibrary", [] { return sceVideodec2TermLibrary(); }); + passed = ReportsNotImplemented("sceVideodec2QueryLibraryInfo", [] { return sceVideodec2QueryLibraryInfo(); }) && passed; + passed = ReportsNotImplemented("sceVideodec2QueryDecoderInfo", [] { return sceVideodec2QueryDecoderInfo(); }) && passed; + passed = ReportsNotImplemented("sceVideodec2MapMemory", [] { return sceVideodec2MapMemory(); }) && passed; + passed = ReportsNotImplemented("sceVideodec2MapDirectMemory", [] { return sceVideodec2MapDirectMemory(nullptr, nullptr); }) && passed; + passed = ReportsNotImplemented("sceVideodec2PresetDecoderConfigInfo", [] { return sceVideodec2PresetDecoderConfigInfo(); }) && passed; + passed = ReportsNotImplemented("sceVideodec2PresetDecoderConfigInfoAvc", [] { return sceVideodec2PresetDecoderConfigInfoAvc(); }) && passed; + passed = ReportsNotImplemented("sceVideodec2PresetDecoderConfigInfoHevc", [] { return sceVideodec2PresetDecoderConfigInfoHevc(); }) && passed; + passed = ReportsNotImplemented("sceVideodec2PresetDecoderConfigInfoVp9", [] { return sceVideodec2PresetDecoderConfigInfoVp9(); }) && passed; + passed = ReportsNotImplemented("sceVideodec2CreateDecoderBid", [] { return sceVideodec2CreateDecoderBid(); }) && passed; + passed = ReportsNotImplemented("sceVideodec2CreateHevcDecoder", [] { return sceVideodec2CreateHevcDecoder(); }) && passed; + passed = ReportsNotImplemented("sceVideodec2QueryHevcDecoderMemoryInfo", [] { return sceVideodec2QueryHevcDecoderMemoryInfo(); }) && passed; + passed = ReportsNotImplemented("sceVideodec2GetHevcPictureInfo", [] { return sceVideodec2GetHevcPictureInfo(); }) && passed; + return passed ? 0 : 1; +} diff --git a/core/libs/tests/GuestVoice.cpp b/core/libs/tests/GuestVoice.cpp index 98c2b5f187..a8671553de 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,10 @@ 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 sceVoicePausePortAll(void); +int APS5_VABI sceVoiceResumePortAll(void); int APS5_VABI sceVoiceGetResourceInfo(VoiceResourceInfo*); int APS5_VABI sceVoiceEnableChat(void); int APS5_VABI sceVoiceDisableChat(void); @@ -25,6 +30,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 +41,29 @@ 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); + Require(sceVoicePausePortAll() == NotInitialized); + Require(sceVoiceResumePortAll() == NotInitialized); +} + std::uint32_t CreatePort(std::int32_t type, float volume) { VoicePortParam param{}; param.port_type = type; @@ -46,13 +77,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 +110,22 @@ 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); + Require(sceVoicePausePortAll() == 0); + Require(sceVoicePausePortAll() == 0); + Require(sceVoiceGetPortInfo(input, &paused) == 0); + Require(paused.port_type == before.port_type && paused.state == before.state && paused.frame_size == before.frame_size); + Require(sceVoiceResumePortAll() == 0); + Require(sceVoiceResumePortAll() == 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 +140,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/GuestVoiceQoS.cpp b/core/libs/tests/GuestVoiceQoS.cpp index cfe4c4f3bb..49d2671371 100644 --- a/core/libs/tests/GuestVoiceQoS.cpp +++ b/core/libs/tests/GuestVoiceQoS.cpp @@ -1,6 +1,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include #include extern "C" { @@ -14,10 +15,34 @@ int APS5_VABI sceVoiceQoSConnect(std::int32_t*, std::int32_t, std::int32_t); int APS5_VABI sceVoiceQoSDisconnect(std::int32_t); int APS5_VABI sceVoiceQoSSetLocalEndpointAttribute(std::int32_t, std::int32_t, const void*, std::int32_t); int APS5_VABI sceVoiceQoSSetRemoteEndpointAttribute(std::int32_t, std::int32_t, const void*, std::int32_t); +int APS5_VABI sceVoiceQoSGetLocalEndpointAttribute(std::int32_t, std::int32_t, void*, std::int32_t); +int APS5_VABI sceVoiceQoSGetRemoteEndpointAttribute(std::int32_t, std::int32_t, const void*, std::int32_t); +int APS5_VABI sceVoiceQoSGetConnectionAttribute(std::int32_t, std::int32_t, void*, std::int32_t); +int APS5_VABI sceVoiceQoSGetLocalEndpoint(std::int32_t, std::int32_t*); +int APS5_VABI sceVoiceQoSGetRemoteEndpoint(std::int32_t, std::int32_t*); +int APS5_VABI sceVoiceQoSEnableChat(); +int APS5_VABI sceVoiceQoSDisableChat(); +int APS5_VABI sceVoiceQoSGetStatus(std::int32_t, std::int32_t, void*, std::int32_t); +int APS5_VABI sceVoiceQoSDebugGetStatus(); +int APS5_VABI sceVoiceQoSInitHQ(); +int APS5_VABI sceVoiceQoSSetMode(); +int APS5_VABI sceVoiceQoSTerminate(); +int APS5_VABI sceVoiceQoSReadPacket(std::int32_t, void*, std::uint32_t*); +int APS5_VABI sceVoiceQoSWritePacket(std::int32_t, const void*, std::uint32_t*); } static void Require(bool value) { if (!value) std::abort(); } +template +static bool NotImplemented(TCall call) { + try { + call(); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + int main(int argc, char** argv) { Require(argc == 2); constexpr int argumentInvalid = static_cast(0x804E0902); @@ -26,6 +51,10 @@ int main(int argc, char** argv) { const auto appType = static_cast(std::strtoul(argv[1], nullptr, 16)); std::vector memory(0x40000, 0xAA); Require(sceVoiceQoSEnd() == notInitialized); + std::int32_t uninitLocal = -1; + std::int32_t uninitRemote = -1; + Require(sceVoiceQoSGetLocalEndpoint(1, &uninitLocal) == notInitialized); + Require(sceVoiceQoSGetRemoteEndpoint(1, &uninitRemote) == notInitialized); Require(sceVoiceQoSInit(nullptr, 0x40000, appType) == argumentInvalid); Require(sceVoiceQoSInit(nullptr, 0, appType) == argumentInvalid); for (std::uint32_t size : {0u, 1u, 0x100u, 0x3FFFFu, 0x40001u, 0x80000u, 0xFFFFFFFFu}) Require(sceVoiceQoSInit(memory.data(), size, appType) == argumentInvalid); @@ -45,6 +74,14 @@ int main(int argc, char** argv) { Require(sceVoiceQoSCreateRemoteEndpoint(&remote) == 0 && remote > 0 && remote != local); Require(sceVoiceQoSConnect(&connection, remote, local) == argumentInvalid); Require(sceVoiceQoSConnect(&connection, local, remote) == 0 && connection > 0); + std::int32_t queriedLocal = -1; + std::int32_t queriedRemote = -1; + Require(sceVoiceQoSGetLocalEndpoint(connection, nullptr) == argumentInvalid); + Require(sceVoiceQoSGetRemoteEndpoint(connection, nullptr) == argumentInvalid); + Require(sceVoiceQoSGetLocalEndpoint(connection + 100, &queriedLocal) == argumentInvalid); + Require(sceVoiceQoSGetRemoteEndpoint(connection + 100, &queriedRemote) == argumentInvalid); + Require(sceVoiceQoSGetLocalEndpoint(connection, &queriedLocal) == 0 && queriedLocal == local); + Require(sceVoiceQoSGetRemoteEndpoint(connection, &queriedRemote) == 0 && queriedRemote == remote); std::int32_t duplicate = -1; Require(sceVoiceQoSConnect(&duplicate, local, remote) == 0 && duplicate > 0 && duplicate != connection); Require(sceVoiceQoSDisconnect(duplicate) == 0); @@ -58,7 +95,14 @@ int main(int argc, char** argv) { Require(sceVoiceQoSDisconnect(otherConnection) == 0); const std::uint32_t level = 0x1234; + std::uint32_t readLevel = 0; + Require(sceVoiceQoSGetLocalEndpointAttribute(local, 3, &readLevel, sizeof(readLevel)) == 0 && readLevel == 0); Require(sceVoiceQoSSetLocalEndpointAttribute(local, 3, &level, sizeof(level)) == 0); + Require(sceVoiceQoSGetLocalEndpointAttribute(local, 3, &readLevel, sizeof(readLevel)) == 0 && readLevel == level); + Require(sceVoiceQoSGetLocalEndpointAttribute(local, 3, nullptr, sizeof(readLevel)) == argumentInvalid); + Require(sceVoiceQoSGetLocalEndpointAttribute(local, 3, &readLevel, 0) == argumentInvalid); + Require(sceVoiceQoSGetLocalEndpointAttribute(local, 3, &readLevel, -1) == argumentInvalid); + Require(sceVoiceQoSGetLocalEndpointAttribute(local + 100, 3, &readLevel, sizeof(readLevel)) == argumentInvalid); Require(sceVoiceQoSSetLocalEndpointAttribute(local, 3, nullptr, sizeof(level)) == argumentInvalid); Require(sceVoiceQoSSetLocalEndpointAttribute(local, 3, &level, 0) == 0); Require(sceVoiceQoSSetLocalEndpointAttribute(local, 3, &level, -1) == 0); @@ -70,8 +114,23 @@ int main(int argc, char** argv) { Require(sceVoiceQoSSetRemoteEndpointAttribute(remote, 3, &level, -1) == 0); Require(sceVoiceQoSSetRemoteEndpointAttribute(remote + 100, 3, &level, sizeof(level)) == argumentInvalid); + std::vector packet(1024, 0); + std::uint32_t packetSize = static_cast(packet.size()); + Require(sceVoiceQoSReadPacket(connection, packet.data(), &packetSize) == 0 && packetSize == 0); + Require(sceVoiceQoSReadPacket(connection, nullptr, &packetSize) == argumentInvalid); + Require(sceVoiceQoSReadPacket(connection, packet.data(), nullptr) == argumentInvalid); + Require(sceVoiceQoSReadPacket(connection + 100, packet.data(), &packetSize) == argumentInvalid); + + packetSize = 100; + Require(sceVoiceQoSWritePacket(connection, packet.data(), &packetSize) == 0); + Require(sceVoiceQoSWritePacket(connection, nullptr, &packetSize) == argumentInvalid); + Require(sceVoiceQoSWritePacket(connection, packet.data(), nullptr) == argumentInvalid); + Require(sceVoiceQoSWritePacket(connection + 100, packet.data(), &packetSize) == argumentInvalid); + Require(sceVoiceQoSDisconnect(connection) == 0); Require(sceVoiceQoSDisconnect(connection) == argumentInvalid); + Require(sceVoiceQoSGetLocalEndpoint(connection, &queriedLocal) == argumentInvalid); + Require(sceVoiceQoSGetRemoteEndpoint(connection, &queriedRemote) == argumentInvalid); Require(sceVoiceQoSDeleteLocalEndpoint(local) == 0); Require(sceVoiceQoSDeleteLocalEndpoint(local) == argumentInvalid); Require(sceVoiceQoSDeleteRemoteEndpoint(remote) == 0); @@ -79,12 +138,27 @@ int main(int argc, char** argv) { Require(sceVoiceQoSCreateRemoteEndpoint(&remote) == 0); Require(sceVoiceQoSEnd() == 0); Require(sceVoiceQoSEnd() == notInitialized); + Require(sceVoiceQoSGetLocalEndpoint(connection, &queriedLocal) == notInitialized); + Require(sceVoiceQoSGetRemoteEndpoint(connection, &queriedRemote) == notInitialized); Require(sceVoiceQoSDeleteRemoteEndpoint(remote) == notInitialized); Require(sceVoiceQoSCreateLocalEndpoint(&local, 1, 0, 0) == notInitialized); Require(sceVoiceQoSSetRemoteEndpointAttribute(remote, 3, &level, sizeof(level)) == notInitialized); + Require(sceVoiceQoSGetLocalEndpointAttribute(local, 3, &readLevel, sizeof(readLevel)) == notInitialized); + Require(sceVoiceQoSReadPacket(connection, packet.data(), &packetSize) == notInitialized); + Require(sceVoiceQoSWritePacket(connection, packet.data(), &packetSize) == notInitialized); Require(sceVoiceQoSInit(memory.data(), 0x40000, appType) == 0); Require(sceVoiceQoSDeleteRemoteEndpoint(remote) == argumentInvalid); Require(sceVoiceQoSEnd() == 0); Require(sceVoiceQoSEnd() == notInitialized); Require(sceVoiceQoSInit(nullptr, 0x40000, appType) == argumentInvalid); + + Require(NotImplemented([] { sceVoiceQoSGetRemoteEndpointAttribute(1, 1, nullptr, 0); })); + Require(NotImplemented([] { sceVoiceQoSGetConnectionAttribute(1, 1, nullptr, 0); })); + Require(NotImplemented([] { sceVoiceQoSEnableChat(); })); + Require(NotImplemented([] { sceVoiceQoSDisableChat(); })); + Require(NotImplemented([] { sceVoiceQoSGetStatus(1, 1, nullptr, 0); })); + Require(NotImplemented([] { sceVoiceQoSDebugGetStatus(); })); + Require(NotImplemented([] { sceVoiceQoSInitHQ(); })); + Require(NotImplemented([] { sceVoiceQoSSetMode(); })); + Require(NotImplemented([] { sceVoiceQoSTerminate(); })); } diff --git a/core/libs/tests/GuestVrSetupDialog.cpp b/core/libs/tests/GuestVrSetupDialog.cpp index e747df363a..56a48ccdd0 100644 --- a/core/libs/tests/GuestVrSetupDialog.cpp +++ b/core/libs/tests/GuestVrSetupDialog.cpp @@ -6,6 +6,7 @@ extern "C" { int APS5_VABI sceVrSetupDialogInitialize(void); int APS5_VABI sceVrSetupDialogOpen(const void* param); int APS5_VABI sceVrSetupDialogUpdateStatus(void); +int APS5_VABI sceVrSetupDialogGetStatus(void); int APS5_VABI sceVrSetupDialogGetResult(void* result); int APS5_VABI sceVrSetupDialogClose(void); int APS5_VABI sceVrSetupDialogTerminate(void); @@ -34,16 +35,19 @@ int main() { Require(sceVrSetupDialogOpen(¶m) == kErrNotInitialized); Require(sceVrSetupDialogClose() == kErrNotInitialized); Require(sceVrSetupDialogGetResult(result) == kErrNotInitialized); + Require(sceVrSetupDialogGetStatus() == kStatusNone); Require(sceVrSetupDialogUpdateStatus() == kStatusNone); Require(sceVrSetupDialogInitialize() == 0); Require(sceVrSetupDialogInitialize() == kErrAlreadyInitialized); + Require(sceVrSetupDialogGetStatus() == kStatusInitialized); Require(sceVrSetupDialogUpdateStatus() == kStatusInitialized); Require(sceVrSetupDialogOpen(nullptr) == kErrArgNull); Require(sceVrSetupDialogGetResult(result) == kErrNotFinished); Require(sceVrSetupDialogOpen(¶m) == 0); + Require(sceVrSetupDialogGetStatus() == kStatusFinished); Require(sceVrSetupDialogUpdateStatus() == kStatusFinished); Require(sceVrSetupDialogGetResult(nullptr) == kErrArgNull); @@ -54,5 +58,15 @@ int main() { Require(sceVrSetupDialogClose() == 0); Require(sceVrSetupDialogTerminate() == 0); Require(sceVrSetupDialogTerminate() == kErrNotInitialized); + Require(sceVrSetupDialogGetStatus() == kStatusNone); Require(sceVrSetupDialogUpdateStatus() == kStatusNone); + + Require(sceVrSetupDialogInitialize() == 0); + Require(sceVrSetupDialogUpdateStatus() == kStatusInitialized); + Require(sceVrSetupDialogClose() == 0); + Require(sceVrSetupDialogUpdateStatus() == kStatusFinished); + result[0] = -1; + Require(sceVrSetupDialogGetResult(result) == 0); + Require(result[0] == kResultUserCanceled); + Require(sceVrSetupDialogTerminate() == 0); } diff --git a/core/libs/tests/GuestVrTracker2.cpp b/core/libs/tests/GuestVrTracker2.cpp new file mode 100644 index 0000000000..f8d5899a66 --- /dev/null +++ b/core/libs/tests/GuestVrTracker2.cpp @@ -0,0 +1,115 @@ +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include +#include +#include + +extern "C" { +std::int32_t APS5_VABI VrTracker2NotInitialized(); +std::int32_t APS5_VABI sceVrTracker2Finalize(); +std::int32_t APS5_VABI VrTracker2ClearWideResult(void* result); +std::int32_t APS5_VABI VrTracker2ClearNarrowResult(void* result); +std::int32_t APS5_VABI VrTracker2CheckSecondThenReportNotInitialized(const void* first, const void* second); +std::int32_t APS5_VABI VrTracker2Accept(); +std::int32_t APS5_VABI VrTracker2FillDefaults(void* output); +std::int32_t APS5_VABI VrTracker2ClearWord(void* output); +std::int32_t APS5_VABI sceVrTracker2QueryMemory(const void* parameter, void* requirement); +std::int32_t APS5_VABI sceVrTracker2Initialize(); +} + +namespace { + +constexpr std::int32_t NotInitialized = static_cast(0x8A740001u); +constexpr std::int32_t InvalidArgument = static_cast(0x8A740006u); + +void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "VrTracker2: %s\n", message); + std::abort(); + } +} + +std::array Filled() { + std::array buffer{}; + buffer.fill(0xAA); + return buffer; +} + +bool AllEqual(const std::uint8_t* begin, const std::uint8_t* end, std::uint8_t value) { + for (; begin != end; ++begin) + if (*begin != value) return false; + return true; +} + +template +bool Throws(TCall call) { + try { + call(); + } catch (const std::exception&) { + return true; + } + return false; +} + +} + +int main() { + Require(VrTracker2NotInitialized() == NotInitialized, "plain export"); + Require(sceVrTracker2Finalize() == NotInitialized, "named export"); + + auto wide = Filled(); + Require(VrTracker2ClearWideResult(wide.data()) == NotInitialized, "wide result return"); + Require(AllEqual(wide.data(), wide.data() + 24, 0) && AllEqual(wide.data() + 24, wide.data() + 32, 0xAA), "wide result clears 24 bytes"); + Require(VrTracker2ClearWideResult(nullptr) == InvalidArgument, "wide result null"); + + auto narrow = Filled(); + Require(VrTracker2ClearNarrowResult(narrow.data()) == NotInitialized, "narrow result return"); + Require(AllEqual(narrow.data(), narrow.data() + 16, 0) && AllEqual(narrow.data() + 16, narrow.data() + 32, 0xAA), "narrow result clears 16 bytes"); + Require(VrTracker2ClearNarrowResult(nullptr) == InvalidArgument, "narrow result null"); + + auto second = Filled(); + Require(VrTracker2CheckSecondThenReportNotInitialized(nullptr, second.data()) == NotInitialized, "second checked, first null"); + Require(VrTracker2CheckSecondThenReportNotInitialized(second.data(), nullptr) == InvalidArgument, "second null"); + Require(AllEqual(second.data(), second.data() + 32, 0xAA), "second untouched"); + + Require(VrTracker2Accept() == 0, "accepting export"); + + auto defaults = Filled(); + Require(VrTracker2FillDefaults(defaults.data()) == 0, "defaults return"); + const std::array expected{0xAA, 0xAA, 0xAA, 0xAA, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x40, 0x00}; + Require(std::memcmp(defaults.data(), expected.data(), expected.size()) == 0, "defaults layout"); + Require(AllEqual(defaults.data() + 16, defaults.data() + 32, 0xAA), "defaults stop at 16 bytes"); + Require(Throws([] { VrTracker2FillDefaults(nullptr); }), "defaults null"); + + auto word = Filled(); + Require(VrTracker2ClearWord(word.data()) == 0, "clear word return"); + Require(AllEqual(word.data(), word.data() + 4, 0) && AllEqual(word.data() + 4, word.data() + 32, 0xAA), "clear word clears 4 bytes"); + Require(Throws([] { VrTracker2ClearWord(nullptr); }), "clear word null"); + + std::array parameter{}; + parameter[0] = 0x20; + parameter.back() = 0xAA; + std::array requirement{}; + requirement.fill(0xAA); + Require(sceVrTracker2QueryMemory(parameter.data(), requirement.data()) == 0, "query return"); + const std::array fields{0x20, 0, 0, 0, 0, 0, 0, 0x02, 0, 0, 0x01, 0}; + Require(std::memcmp(requirement.data(), fields.data(), fields.size()) == 0, "query fields"); + Require(AllEqual(requirement.data() + 12, requirement.data() + 32, 0) && AllEqual(requirement.data() + 32, requirement.data() + 40, 0xAA), "query writes 32 bytes"); + Require(sceVrTracker2QueryMemory(nullptr, requirement.data()) == InvalidArgument, "query null parameter"); + Require(sceVrTracker2QueryMemory(parameter.data(), nullptr) == InvalidArgument, "query null requirement"); + for (std::size_t offset = 4; offset < 32; ++offset) { + auto rejected = parameter; + rejected[offset] = 1; + requirement.fill(0xAA); + Require(sceVrTracker2QueryMemory(rejected.data(), requirement.data()) == InvalidArgument, "query reserved byte"); + Require(AllEqual(requirement.data(), requirement.data() + 40, 0xAA), "query rejection leaves output"); + } + parameter[0] = 0x21; + Require(sceVrTracker2QueryMemory(parameter.data(), requirement.data()) == InvalidArgument, "query size"); + + Require(Throws([] { sceVrTracker2Initialize(); }), "unknown export"); + return 0; +} diff --git a/core/libs/tests/GuestWcsrtombsS.cpp b/core/libs/tests/GuestWcsrtombsS.cpp index 758ff5d33c..44a4206540 100644 --- a/core/libs/tests/GuestWcsrtombsS.cpp +++ b/core/libs/tests/GuestWcsrtombsS.cpp @@ -5,6 +5,7 @@ extern "C" int APS5_VABI wcsrtombs_s_nid_postfix( std::size_t*, char*, std::size_t, const std::uint16_t**, std::size_t, void*); +extern "C" int APS5_VABI wcstombs_s_nid_postfix(std::size_t*, char*, std::size_t, const std::uint16_t*, std::size_t); static void Require(bool value) { if (!value) std::abort(); } constexpr auto Failed = static_cast(-1); @@ -75,4 +76,22 @@ int main() { result = 7; Require(wcsrtombs_s_nid_postfix(&result, out, 4, &source, 4, nullptr) == 22 && result == Failed && out[0] == '\0'); Require(source == text && std::memcmp(out, "\0xxx", 4) == 0); + + char narrow[8]; + std::memset(narrow, 'x', sizeof(narrow)); + Require(wcstombs_s_nid_postfix(&result, narrow, sizeof(narrow), text, sizeof(narrow)) == 0 && result == 3); + Require(std::memcmp(narrow, "abc\0x", 5) == 0); + std::memset(narrow, 'x', sizeof(narrow)); + Require(wcstombs_s_nid_postfix(&result, narrow, sizeof(narrow), text, 2) == 0 && result == 2); + Require(std::memcmp(narrow, "ab\0x", 4) == 0); + Require(wcstombs_s_nid_postfix(&result, nullptr, 0, text, 0) == 0 && result == 3); + std::memset(narrow, 'x', sizeof(narrow)); + Require(wcstombs_s_nid_postfix(&result, narrow, 3, text, 3) == 34 && result == Failed && narrow[0] == '\0'); + Require(wcstombs_s_nid_postfix(&result, nullptr, 4, text, 4) == 22 && result == Failed); + std::memset(narrow, 'x', sizeof(narrow)); + Require(wcstombs_s_nid_postfix(&result, narrow, sizeof(narrow), nullptr, sizeof(narrow)) == 22); + Require(result == Failed && narrow[0] == '\0'); + std::memset(narrow, 'x', sizeof(narrow)); + Require(wcstombs_s_nid_postfix(&result, narrow, sizeof(narrow), wide, sizeof(narrow)) == 86 && result == Failed); + Require(std::memcmp(narrow, "a\0xx", 4) == 0); } diff --git a/core/libs/tests/GuestWebBrowserDialog.cpp b/core/libs/tests/GuestWebBrowserDialog.cpp index 9fc91930ac..ea5d58c2cc 100644 --- a/core/libs/tests/GuestWebBrowserDialog.cpp +++ b/core/libs/tests/GuestWebBrowserDialog.cpp @@ -80,5 +80,15 @@ int main() { Require(sceWebBrowserDialogTerminate() == 0); Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); + Require(sceWebBrowserDialogInitialize() == 0); + Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_INITIALIZED); + Require(sceWebBrowserDialogClose() == 0); + Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_FINISHED); + result = 0x5a5a5a5a; + Require(sceWebBrowserDialogGetResult(&result) == 0); + Require(result == COMMON_DIALOG_RESULT_USER_CANCELED); + Require(sceWebBrowserDialogTerminate() == 0); + Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); + return 0; } diff --git a/core/libs/tests/GuestWideConversion.cpp b/core/libs/tests/GuestWideConversion.cpp index 8cd823acbe..07b28e68ca 100644 --- a/core/libs/tests/GuestWideConversion.cpp +++ b/core/libs/tests/GuestWideConversion.cpp @@ -69,7 +69,7 @@ static void CheckStoul() { CheckStoul(u"017", 0, 15, 3, 0, "octal with base 0"); CheckStoul(u"z", 36, 35, 1, 0, "base 36"); CheckStoul(u"0b1", 0, 0, 1, 0, "no binary prefix"); - CheckStoul(u"0xg", 16, 0, 1, 0, "prefix without digits"); + CheckStoul(u"0xg", 16, 0, 0, 0, "prefix without digits"); CheckStoul(u"12é3", 10, 12, 2, 0, "stops at a non-ASCII unit"); CheckStoul(u" 5", 10, 0, 0, 0, "non-ASCII space is not skipped"); CheckStoul(u"", 10, 0, 0, 0, "empty string"); @@ -118,6 +118,8 @@ static void CheckScan() { Require(swscanf_s_nid_postfix(u"-2", u"%hd", &half) == 1 && half == -2, "%hd"); unsigned value = 0; Require(swscanf_s_nid_postfix(u"0xg", u"%x", &value) == 0, "%x prefix without digits is a matching failure"); + void* pointer = nullptr; + Require(swscanf_s_nid_postfix(u"0x7ffe12345678 5", u"%p%d", &pointer, &first) == 2 && reinterpret_cast(pointer) == 0x7ffe12345678u && first == 5, "%p takes no size"); char16_t text[6] = {u'?'}; Require(swscanf_s_nid_postfix(u"hello world", u"%ls", text, std::size_t{6}) == 1 && std::u16string(text) == u"hello", "%ls fits"); @@ -169,6 +171,14 @@ static void CheckUnsizedScan() { Require(swscanf_nid_postfix(u"-2", u"%hd", &half) == 1 && half == -2, "swscanf %hd"); unsigned value = 0; Require(swscanf_nid_postfix(u"0xg", u"%x", &value) == 0, "swscanf %x prefix without digits is a matching failure"); + void* pointer = nullptr; + Require(swscanf_nid_postfix(u"0x7ffe12345678", u"%p", &pointer) == 1 && reinterpret_cast(pointer) == 0x7ffe12345678u, "swscanf %p stores a whole pointer"); + Require(swscanf_nid_postfix(u"DeadBeef", u"%p", &pointer) == 1 && reinterpret_cast(pointer) == 0xdeadbeefu, "swscanf %p without a prefix"); + void* next = nullptr; + Require(swscanf_nid_postfix(u"0x1234", u"%4p%p", &pointer, &next) == 2 && reinterpret_cast(pointer) == 0x12u && reinterpret_cast(next) == 0x34u, "swscanf %p width"); + Require(swscanf_nid_postfix(u"0xff 7", u"%*p%d", &first) == 1 && first == 7, "swscanf suppressed %p"); + Require(swscanf_nid_postfix(u"-1", u"%p", &pointer) == 1 && reinterpret_cast(pointer) == std::numeric_limits::max(), "swscanf %p converts as unsigned"); + Require(swscanf_nid_postfix(u"0xg", u"%p", &pointer) == 0, "swscanf %p prefix without digits is a matching failure"); char16_t text[16] = {u'?'}; Require(swscanf_nid_postfix(u"hello world", u"%ls", text) == 1 && std::u16string(text) == u"hello", "swscanf %ls"); @@ -209,6 +219,7 @@ static void CheckUnsizedScan() { Require(swscanf_nid_postfix(u"5", nullptr) == -1, "swscanf null format"); Require(Throws([&] { swscanf_nid_postfix(u"abc", u"%l[a-c]", text); }), "swscanf scanset range throws"); Require(Throws([&] { swscanf_nid_postfix(u"5", u"%Ld", &wide); }), "swscanf %Ld throws"); + Require(Throws([&] { swscanf_nid_postfix(u"1", u"%lp", &pointer); }), "swscanf %lp throws"); Require(Throws([&] { swscanf_nid_postfix(u"Ā", u"%s", narrow); }), "swscanf unconvertible narrow %s throws"); } diff --git a/core/libs/tests/GuestWideFormatting.cpp b/core/libs/tests/GuestWideFormatting.cpp index 818f401ffd..8ebfa2753e 100644 --- a/core/libs/tests/GuestWideFormatting.cpp +++ b/core/libs/tests/GuestWideFormatting.cpp @@ -276,7 +276,33 @@ static void CheckWcsftime() { Require(WcsftimeThrows(buffer, u"%Y", nullptr), "wcsftime null time"); } +static void CheckAlternateOctalZero() { + struct Case { const char16_t* format; const char16_t* expected; unsigned value; }; + for (const auto& test : { + Case{u"%#.0o", u"0", 0}, Case{u"%#.o", u"0", 0}, Case{u"%#.00o", u"0", 0}, + Case{u"[%#5.0o]", u"[ 0]", 0}, Case{u"[%-#5.0o]", u"[0 ]", 0}, + Case{u"[%#05.0o]", u"[ 0]", 0}, Case{u"%.0o", u"", 0}, Case{u"%#.0x", u"", 0}, + Case{u"%#.0o", u"01", 1}, Case{u"%#.1o", u"0", 0}, Case{u"%#.3o", u"000", 0}, + Case{u"%#.0hho", u"0", 256}, Case{u"%#.0ho", u"0", 65536} + }) { + char16_t buffer[32]; + const auto expected = std::u16string(test.expected); + Require(swprintf_nid_postfix(buffer, 32, test.format, test.value) == static_cast(expected.size()) && buffer == expected, "swprintf alternate octal zero"); + Require(Format(buffer, 32, test.format, test.value) == static_cast(expected.size()) && buffer == expected, "vswprintf alternate octal zero"); + Require(snwprintf_s_nid_postfix(buffer, 32, test.format, test.value) == static_cast(expected.size()) && buffer == expected, "snwprintf_s alternate octal zero"); + } + char16_t buffer[16]; + Require(swprintf_nid_postfix(buffer, 16, u"%#.*o", 0, 0u) == 1 && buffer == std::u16string(u"0"), "octal zero dynamic precision"); + Require(swprintf_nid_postfix(buffer, 16, u"%#.*o", -1, 0u) == 1 && buffer == std::u16string(u"0"), "octal zero omitted precision"); + Require(swprintf_nid_postfix(buffer, 16, u"%2$#.*1$o", 0, 0u) == 1 && buffer == std::u16string(u"0"), "octal zero positional precision"); + Require(swprintf_nid_postfix(buffer, 16, u"%#.0llo", 0ull) == 1 && buffer == std::u16string(u"0"), "octal zero long long"); + buffer[0] = u'x'; + buffer[1] = u'y'; + Require(snwprintf_s_nid_postfix(buffer, 1, u"%#.0o", 0u) == 1 && buffer[0] == 0 && buffer[1] == u'y', "octal zero bounded output"); +} + int main() { + CheckAlternateOctalZero(); CheckBounded(); CheckPositional(); CheckPointer(); diff --git a/core/libs/tests/GuestWideStreamScan.cpp b/core/libs/tests/GuestWideStreamScan.cpp new file mode 100644 index 0000000000..0da21861b0 --- /dev/null +++ b/core/libs/tests/GuestWideStreamScan.cpp @@ -0,0 +1,164 @@ +#include "prx/libc/include/FileStream.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include + +extern "C" { +extern FileStream* __stdinp_nid_postfix; +int APS5_VABI vfwscanf_nid_postfix(FileStream*, const char16_t*, void*); +int APS5_VABI vfwscanf_s_nid_postfix(FileStream*, const char16_t*, void*); +int APS5_VABI vwscanf_nid_postfix(const char16_t*, void*); +int APS5_VABI vwscanf_s_nid_postfix(const char16_t*, void*); +int APS5_VABI fwscanf_nid_postfix(FileStream*, const char16_t*, ...); +int APS5_VABI fwscanf_s_nid_postfix(FileStream*, const char16_t*, ...); +int APS5_VABI wscanf_nid_postfix(const char16_t*, ...); +int APS5_VABI wscanf_s_nid_postfix(const char16_t*, ...); +std::int32_t APS5_VABI fgetwc_nid_postfix(FileStream*); +} + +#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 char16_t* format, ...) { + APS5_VA_BEGIN(format); + const int result = vfwscanf_nid_postfix(stream, format, args); + APS5_VA_END(); + return result; +} + +static int APS5_VABI ScanStreamSized(FileStream* stream, const char16_t* format, ...) { + APS5_VA_BEGIN(format); + const int result = vfwscanf_s_nid_postfix(stream, format, args); + APS5_VA_END(); + return result; +} + +static int APS5_VABI ScanInput(const char16_t* format, ...) { + APS5_VA_BEGIN(format); + const int result = vwscanf_nid_postfix(format, args); + APS5_VA_END(); + return result; +} + +static int APS5_VABI ScanInputSized(const char16_t* format, ...) { + APS5_VA_BEGIN(format); + const int result = vwscanf_s_nid_postfix(format, args); + APS5_VA_END(); + return result; +} + +static FileStream* Open(const char* text) { + auto* stream = new FileStream(std::tmpfile()); + Require(std::fputs(text, stream->GetHandle()) >= 0, "seed the stream"); + std::rewind(stream->GetHandle()); + return stream; +} + +static void Close(FileStream* stream) { + stream->Close(); + delete stream; +} + +static void CheckConsumption() { + std::int32_t value = -1; + std::int64_t count = -1; + char16_t word[8]{}; + + FileStream* stream = Open(" 42 rest"); + Require(ScanStream(stream, u"%d", &value) == 1 && value == 42, "an integer after leading space"); + Require(fgetwc_nid_postfix(stream) == u' ', "the character after the field stays in the stream"); + Require(fwscanf_nid_postfix(stream, u"%ls", word) == 1 && std::u16string(word) == u"rest", "a following fwscanf continues"); + Require(fwscanf_nid_postfix(stream, u"%d", &value) == EOF, "end of file before any conversion"); + Close(stream); + + stream = Open("0xg"); + std::uint32_t hex = 7; + Require(ScanStream(stream, u"%x", &hex) == 0 && hex == 7, "a hexadecimal prefix without digits is a matching failure"); + Require(fgetwc_nid_postfix(stream) == u'g', "the prefix is consumed and the next character is pushed back"); + Close(stream); + + stream = Open("12abc"); + Require(ScanStream(stream, u"%d%ln", &value, &count) == 1 && value == 12 && count == 2, "%ln counts the consumed characters"); + Require(fgetwc_nid_postfix(stream) == u'a', "the field stops before the first letter"); + Close(stream); + + stream = Open("ab"); + Require(ScanStream(stream, u"ac") == 0, "a literal mismatch"); + Require(fgetwc_nid_postfix(stream) == u'b', "the mismatching character is pushed back"); + Close(stream); + + stream = Open("x"); + Require(ScanStream(stream, u"%d", &value) == 0, "a letter is not an integer"); + Require(fgetwc_nid_postfix(stream) == u'x', "the rejected letter is pushed back"); + Close(stream); + + stream = Open("-"); + Require(ScanStream(stream, u"%d", &value) == 0, "a lone sign is a matching failure"); + Require(fgetwc_nid_postfix(stream) == -1, "the sign is consumed"); + Close(stream); + + stream = Open(" "); + Require(ScanStream(stream, u"%d", &value) == EOF, "only white space"); + Close(stream); + + stream = Open("2.5x"); + double real = 0; + Require(ScanStream(stream, u"%lf", &real) == 1 && real == 2.5, "a float"); + Require(fgetwc_nid_postfix(stream) == u'x', "the character after the float"); + Close(stream); +} + +static void CheckWideText() { + std::int32_t value = 0; + char16_t letters[8]{}; + FileStream* stream = Open("5 ab\xc3\xa9t\xc3\xa9"); + Require(ScanStream(stream, u"%d %l[abc]", &value, letters) == 2 && value == 5 && std::u16string(letters) == u"ab", "a scanset stops at a non-ASCII character"); + Require(fgetwc_nid_postfix(stream) == 0xe9, "a non-ASCII lookahead goes back as UTF-8"); + Require(fwscanf_nid_postfix(stream, u"%ls", letters) == 1 && std::u16string(letters) == u"té", "the rest decodes"); + Close(stream); + + stream = Open("hello world"); + char16_t small[4]{}; + Require(ScanStreamSized(stream, u"%3ls", small, 4u) == 1 && std::u16string(small) == u"hel", "vfwscanf_s %ls with a size"); + Require(fwscanf_s_nid_postfix(stream, u"%ls", small, 4u) == 1 && std::u16string(small) == u"lo", "fwscanf_s %ls that fits"); + Require(fwscanf_s_nid_postfix(stream, u"%ls", small, 4u) == 0 && small[0] == 0, "fwscanf_s rejects a too small buffer"); + Close(stream); + + Require(fwscanf_nid_postfix(nullptr, u"%d", &value) == EOF, "null stream"); + Require(vfwscanf_s_nid_postfix(nullptr, u"%d", nullptr) == EOF, "null stream with sizes"); +} + +static void CheckStandardInput() { + FileStream* const standardInput = __stdinp_nid_postfix; + FileStream* stream = Open("-5 12 ab cd"); + __stdinp_nid_postfix = stream; + std::int32_t first = 0, second = 0; + char16_t word[4]{}; + Require(ScanInput(u"%d", &first) == 1 && first == -5, "vwscanf reads the guest standard input"); + Require(wscanf_nid_postfix(u"%d", &second) == 1 && second == 12, "wscanf reads the guest standard input"); + Require(ScanInputSized(u"%ls", word, 4u) == 1 && std::u16string(word) == u"ab", "vwscanf_s"); + Require(wscanf_s_nid_postfix(u"%ls", word, 4u) == 1 && std::u16string(word) == u"cd", "wscanf_s"); + __stdinp_nid_postfix = standardInput; + Close(stream); +} + +int main() { + CheckConsumption(); + CheckWideText(); + CheckStandardInput(); + return 0; +} diff --git a/core/libs/tests/GuestWideStrings.cpp b/core/libs/tests/GuestWideStrings.cpp index abbba3196d..2e22ad3144 100644 --- a/core/libs/tests/GuestWideStrings.cpp +++ b/core/libs/tests/GuestWideStrings.cpp @@ -314,6 +314,22 @@ int main() { require(wcstoull_nid_postfix(binary, &end, base) == 0 && end == binary + 3); } require(wcstoll_nid_postfix(binary + 2, &end, 16) == 0xb101 && end == binary + 7); + struct SignedCase { + const char16_t* text; + int base; + long long value; + std::ptrdiff_t consumed; + int error; + }; + for (const SignedCase& item : {SignedCase{u"- 1", 10, -1, 3, 0}, SignedCase{u"+\t-1", 10, LLONG_MAX, 4, 34}, SignedCase{u" - -1", 10, LLONG_MIN, 5, 34}, + SignedCase{u"-18446744073709551615", 10, LLONG_MIN, 21, 34}, SignedCase{u"-9223372036854775808", 10, LLONG_MIN, 20, 0}, + SignedCase{u"-9223372036854775809", 10, LLONG_MIN, 20, 34}, SignedCase{u"+-9223372036854775808", 10, LLONG_MAX, 21, 34}, SignedCase{u" -0x", 16, 0, 0, 0}, + SignedCase{u"- ", 10, 0, 0, 0}}) { + for (auto* parse : {wcstol_nid_postfix, wcstoll_nid_postfix, wcstoimax_nid_postfix}) { + *__error_nid_postfix() = 0; + require(parse(item.text, &end, item.base) == item.value && end - item.text == item.consumed && *__error_nid_postfix() == item.error); + } + } const char16_t* scientific = u"3.5e2!"; require(wcstod_nid_postfix(scientific, &end) == 350.0 && end == scientific + 5); @@ -322,12 +338,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/H264Fixture.hpp b/core/libs/tests/H264Fixture.hpp index 20eab25442..c9124ada99 100644 --- a/core/libs/tests/H264Fixture.hpp +++ b/core/libs/tests/H264Fixture.hpp @@ -114,6 +114,18 @@ inline constexpr std::array PictureHashes{ inline constexpr std::array DisplayOrderUnits{0, 2, 3, 1, 5, 4}; +inline constexpr std::size_t SpsOffset = 10; +inline constexpr std::array CroppedSps{ + 0x67, 0x64, 0x00, 0x0a, 0xac, 0xd9, 0x44, 0x7f, 0x29, 0x70, 0x11, 0x00, 0x00, + 0x03, 0x00, 0x01, 0x00, 0x00, 0x03, 0x00, 0x3c, 0x0f, 0x12, 0x25, 0x96, +}; + +inline std::vector croppedStream() { + std::vector stream(Stream.begin(), Stream.end()); + for (std::size_t index = 0; index < CroppedSps.size(); ++index) stream[SpsOffset + index] = CroppedSps[index]; + return stream; +} + inline std::uint64_t hashNv12(const std::uint8_t* buffer, std::uint32_t pitch, std::uint32_t height) { std::uint64_t hash = 0xcbf29ce484222325ull; const auto mix = [&](const std::uint8_t* row) { @@ -125,21 +137,21 @@ inline std::uint64_t hashNv12(const std::uint8_t* buffer, std::uint32_t pitch, s return hash; } -inline std::vector> accessUnits(bool lengthPrefixed) { +inline std::vector> accessUnits(const std::uint8_t* data, std::size_t size, bool lengthPrefixed) { std::vector starts; - for (std::size_t index = 0; index + 4 < Stream.size(); ++index) { - if (Stream[index] == 0 && Stream[index + 1] == 0 && Stream[index + 2] == 0 && Stream[index + 3] == 1 && (Stream[index + 4] & 0x1fu) == 9) starts.push_back(index); + for (std::size_t index = 0; index + 4 < size; ++index) { + if (data[index] == 0 && data[index + 1] == 0 && data[index + 2] == 0 && data[index + 3] == 1 && (data[index + 4] & 0x1fu) == 9) starts.push_back(index); } - starts.push_back(Stream.size()); + starts.push_back(size); std::vector> units; for (std::size_t unit = 0; unit + 1 < starts.size(); ++unit) { if (!lengthPrefixed) { - units.emplace_back(Stream.begin() + static_cast(starts[unit]), Stream.begin() + static_cast(starts[unit + 1])); + units.emplace_back(data + starts[unit], data + starts[unit + 1]); continue; } std::vector nals; for (std::size_t index = starts[unit]; index + 3 < starts[unit + 1]; ++index) { - if (Stream[index] == 0 && Stream[index + 1] == 0 && Stream[index + 2] == 1) { + if (data[index] == 0 && data[index + 1] == 0 && data[index + 2] == 1) { nals.push_back(index + 3); index += 2; } @@ -147,14 +159,18 @@ inline std::vector> accessUnits(bool lengthPrefixed) { std::vector converted; for (std::size_t nal = 0; nal < nals.size(); ++nal) { auto end = nal + 1 < nals.size() ? nals[nal + 1] - 3 : starts[unit + 1]; - while (end > nals[nal] && Stream[end - 1] == 0) --end; + while (end > nals[nal] && data[end - 1] == 0) --end; const auto length = end - nals[nal]; for (int shift = 24; shift >= 0; shift -= 8) converted.push_back(static_cast(length >> shift)); - converted.insert(converted.end(), Stream.begin() + static_cast(nals[nal]), Stream.begin() + static_cast(end)); + converted.insert(converted.end(), data + nals[nal], data + end); } units.push_back(std::move(converted)); } return units; } +inline std::vector> accessUnits(bool lengthPrefixed) { + return accessUnits(Stream.data(), Stream.size(), lengthPrefixed); +} + } diff --git a/core/libs/tests/ImageSampleGather.cpp b/core/libs/tests/ImageSampleGather.cpp index 6fa633e3a7..3d3b40d608 100644 --- a/core/libs/tests/ImageSampleGather.cpp +++ b/core/libs/tests/ImageSampleGather.cpp @@ -72,8 +72,25 @@ static void CheckUnsupportedOpcodeDiagnostic(std::uint32_t encoding, std::string } Require(false); } +static void CheckSampleCompareOffsetAdjust() { + const std::array code{0xf0e00109u, 0x00801e33u}; + const RdnaInstruction instruction = DecodeRdnaMimg(0u, code, 0u); + Require(instruction.op == RdnaOpcode::ImageSample); + Require(instruction.imageOpcodeId == 0xb8u); + Require(instruction.imageSampleFlags == (RdnaImageSampleFlagCompare | RdnaImageSampleFlagOffset | RdnaImageSampleFlagAdjust)); + Require(instruction.imageAddressComponents == 4u); + Require(GetRdnaImageAddressComponentLayout(instruction.imageSampleFlags, 1u).bitOffset == 32u); + IrProgram program; + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + TranslationContext context(program, block, 256); + context.TranslateInstruction(instruction); + Require(program.Resources().memoryInfo.size() == 1u); + Require(program.Resources().memoryInfo.back().imageSampleFlags == instruction.imageSampleFlags); +} int main() { CheckSamplerVccTuple(); + CheckSampleCompareOffsetAdjust(); CheckSamplerVccTupleOverflow(); Check(0x26u, RdnaOpcode::ImageSampleBCl, 0xfu); Check(0x36u, RdnaOpcode::ImageSampleBClO, 0xfu); diff --git a/core/libs/tests/JsonCopyFailure.cpp b/core/libs/tests/JsonCopyFailure.cpp new file mode 100644 index 0000000000..0fa28bad98 --- /dev/null +++ b/core/libs/tests/JsonCopyFailure.cpp @@ -0,0 +1,220 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include + +struct Value { void* node; }; +struct String { void* text; }; +struct Array { void* items; }; +struct Object { void* items; }; +struct Pair { String key; std::uint64_t reserved; Value value; }; + +extern "C" { +void APS5_VABI _ZN3sce4Json5ValueC1Ev(Value*); +void APS5_VABI _ZN3sce4Json5ValueC1EPKc(Value*, const char*); +void APS5_VABI _ZN3sce4Json5ValueC1ERKNS0_6StringE(Value*, const String*); +void APS5_VABI _ZN3sce4Json5ValueC1ERKNS0_5ArrayE(Value*, const Array*); +void APS5_VABI _ZN3sce4Json5ValueC1ERKNS0_6ObjectE(Value*, const Object*); +void APS5_VABI _ZN3sce4Json5ValueC1ENS0_9ValueTypeE(Value*, std::int32_t); +void APS5_VABI _ZN3sce4Json5ValueC1ERKS1_(Value*, const Value*); +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*); +int APS5_VABI _ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(Value*, const char*, std::size_t); +void APS5_VABI _ZN3sce4Json6StringC1Ev(String*); +void APS5_VABI _ZN3sce4Json6StringC1EPKc(String*, const char*); +void APS5_VABI _ZN3sce4Json6StringD1Ev(String*); +const char* APS5_VABI _ZNK3sce4Json6String5c_strEv(const String*); +void APS5_VABI _ZN3sce4Json5ArrayC1Ev(Array*); +void APS5_VABI _ZN3sce4Json5ArrayC1ERKS1_(Array*, const Array*); +void APS5_VABI _ZN3sce4Json5ArrayD1Ev(Array*); +void APS5_VABI _ZN3sce4Json5Array9push_backERKNS0_5ValueE(Array*, const Value*); +std::size_t APS5_VABI _ZNK3sce4Json5Array4sizeEv(const Array*); +const Value* APS5_VABI _ZNK3sce4Json5Array5frontEv(const Array*); +void APS5_VABI _ZN3sce4Json6ObjectC1Ev(Object*); +void APS5_VABI _ZN3sce4Json6ObjectC1ERKS1_(Object*, const Object*); +void APS5_VABI _ZN3sce4Json6ObjectD1Ev(Object*); +Object* APS5_VABI _ZN3sce4Json6ObjectaSERKS1_(Object*, const Object*); +std::size_t APS5_VABI _ZNK3sce4Json6Object4sizeEv(const Object*); +void APS5_VABI _ZN3sce4Json6Object4PairC1ERKNS0_6StringERKNS0_5ValueE(Pair*, const String*, const Value*); +void APS5_VABI _ZN3sce4Json6Object4PairD1Ev(Pair*); +} + +static std::ptrdiff_t allocationBudget = -1; +static std::size_t liveAllocations = 0; +static const char* activeCase = nullptr; +static unsigned activeBudget = 0; + +#ifndef _WIN32 +extern "C" void NotImplemented_nid_no_patch(const char* name) { + std::fprintf(stderr, "unexpected unimplemented JSON export: %s\n", name); + std::abort(); +} +#endif + +void* operator new(std::size_t bytes) { + if (allocationBudget == 0) throw std::bad_alloc(); + if (allocationBudget > 0) --allocationBudget; + if (void* pointer = std::malloc(bytes ? bytes : 1)) { + ++liveAllocations; + return pointer; + } + throw std::bad_alloc(); +} +void operator delete(void* pointer) noexcept { + if (pointer) --liveAllocations; + std::free(pointer); +} +void operator delete(void* pointer, std::size_t) noexcept { ::operator delete(pointer); } + +static void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "%s budget=%u: %s (live=%zu)\n", activeCase, activeBudget, message, liveAllocations); + std::abort(); + } +} + +static void CheckValue(const Value& value, const char* expected) { + String output{}; + _ZN3sce4Json6StringC1Ev(&output); + Require(_ZN3sce4Json5Value9serializeERNS0_6StringE(const_cast(&value), &output) == 0, "serialization failed"); + Require(std::strcmp(_ZNK3sce4Json6String5c_strEv(&output), expected) == 0, "value changed"); + _ZN3sce4Json6StringD1Ev(&output); +} + +enum class Operation { + ValueCopy, ArrayCopy, ObjectCopy, ValueAssign, ObjectAssign, ArrayAppend, + ValueArray, ValueObject, ValueString, ValueText, ValueTypeString, ValueTypeArray, + 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) { + Value result{}; + Array arrayResult{}; + Object objectResult{}; + Pair pair{}; + bool valueReady = false, arrayReady = false, objectReady = false, pairReady = false; + 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; + } + if (operation == Operation::ArrayAppend) { _ZN3sce4Json5ArrayC1Ev(&arrayResult); arrayReady = true; } + if (operation == Operation::ObjectAssign) { _ZN3sce4Json6ObjectC1Ev(&objectResult); objectReady = true; } + bool succeeded = false; + allocationBudget = activeBudget; + try { + switch (operation) { + case Operation::ValueCopy: _ZN3sce4Json5ValueC1ERKS1_(&result, &source); valueReady = true; break; + case Operation::ArrayCopy: _ZN3sce4Json5ArrayC1ERKS1_(&arrayResult, &array); arrayReady = true; break; + case Operation::ObjectCopy: _ZN3sce4Json6ObjectC1ERKS1_(&objectResult, &object); objectReady = true; break; + case Operation::ValueAssign: _ZN3sce4Json5ValueaSERKS1_(&result, &source); break; + case Operation::ObjectAssign: _ZN3sce4Json6ObjectaSERKS1_(&objectResult, &object); break; + case Operation::ArrayAppend: _ZN3sce4Json5Array9push_backERKNS0_5ValueE(&arrayResult, &source); break; + case Operation::ValueArray: _ZN3sce4Json5ValueC1ERKNS0_5ArrayE(&result, &array); valueReady = true; break; + case Operation::ValueObject: _ZN3sce4Json5ValueC1ERKNS0_6ObjectE(&result, &object); valueReady = true; break; + case Operation::ValueString: _ZN3sce4Json5ValueC1ERKNS0_6StringE(&result, &text); valueReady = true; break; + case Operation::ValueText: _ZN3sce4Json5ValueC1EPKc(&result, _ZNK3sce4Json6String5c_strEv(&text)); valueReady = true; break; + case Operation::ValueTypeString: _ZN3sce4Json5ValueC1ENS0_9ValueTypeE(&result, 5); valueReady = true; break; + case Operation::ValueTypeArray: _ZN3sce4Json5ValueC1ENS0_9ValueTypeE(&result, 6); valueReady = true; break; + 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; + } catch (const std::bad_alloc&) { + } + allocationBudget = -1; + if (valueReady) { + if (succeeded) CheckValue(result, expected); + else { + Require(_ZNK3sce4Json5Value7getTypeEv(&result) == 5, "failed setter changed the destination type"); + CheckValue(result, "\"previous\""); + } + _ZN3sce4Json5ValueD1Ev(&result); + } + if (arrayReady) { + const auto count = _ZNK3sce4Json5Array4sizeEv(&arrayResult); + Require(count == (succeeded ? (operation == Operation::ArrayAppend ? 1 : 3) : 0), "array contents changed"); + if (succeeded && operation == Operation::ArrayAppend) CheckValue(*_ZNK3sce4Json5Array5frontEv(&arrayResult), expected); + _ZN3sce4Json5ArrayD1Ev(&arrayResult); + } + if (objectReady) { + Require(_ZNK3sce4Json6Object4sizeEv(&objectResult) == (succeeded ? 2 : 0), "object contents changed"); + _ZN3sce4Json6ObjectD1Ev(&objectResult); + } + if (pairReady) { CheckValue(pair.value, expected); _ZN3sce4Json6Object4PairD1Ev(&pair); } + return succeeded; +} + +int main() { + constexpr const char* objectJson = "{\"long\":\"a sufficiently long string to require its own allocation\",\"nested\":[{\"leaf\":\"another sufficiently long string\"},[true,null,17]]}"; + constexpr const char* arrayJson = "[1,{\"long\":\"a sufficiently long string to require its own allocation\"},[false,null]]"; + constexpr const char* stringJson = "\"a sufficiently long string to require its own allocation\""; + const auto initial = liveAllocations; + Value objectValue{}, arrayValue{}; + String text{}; + _ZN3sce4Json5ValueC1Ev(&objectValue); + _ZN3sce4Json5ValueC1Ev(&arrayValue); + Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&objectValue, objectJson, std::strlen(objectJson)) == 0, "prepare object"); + Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&arrayValue, arrayJson, std::strlen(arrayJson)) == 0, "prepare array"); + _ZN3sce4Json6StringC1EPKc(&text, "a sufficiently long string to require its own allocation"); + const auto* array = _ZNK3sce4Json5Value8getArrayEv(&arrayValue); + const auto* object = _ZNK3sce4Json5Value9getObjectEv(&objectValue); + struct Case { const char* name; Operation operation; const Value* source; const char* expected; }; + const Case cases[] = { + {"value object copy", Operation::ValueCopy, &objectValue, objectJson}, + {"value array copy", Operation::ValueCopy, &arrayValue, arrayJson}, + {"array copy", Operation::ArrayCopy, &arrayValue, arrayJson}, + {"object copy", Operation::ObjectCopy, &objectValue, objectJson}, + {"value assignment", Operation::ValueAssign, &objectValue, objectJson}, + {"object assignment", Operation::ObjectAssign, &objectValue, objectJson}, + {"array append", Operation::ArrayAppend, &objectValue, objectJson}, + {"value from array", Operation::ValueArray, &arrayValue, arrayJson}, + {"value from object", Operation::ValueObject, &objectValue, objectJson}, + {"value from string", Operation::ValueString, &objectValue, stringJson}, + {"value from text", Operation::ValueText, &objectValue, stringJson}, + {"empty string value", Operation::ValueTypeString, &objectValue, "\"\""}, + {"empty array value", Operation::ValueTypeArray, &objectValue, "[]"}, + {"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; + for (const auto& test : cases) { + activeCase = test.name; + bool succeeded = false; + for (activeBudget = 0; activeBudget < 256 && !succeeded; ++activeBudget) { + const auto before = liveAllocations; + succeeded = Run(test.operation, *test.source, test.expected, *array, *object, text); + if (liveAllocations != before) std::fprintf(stderr, "expected %zu live allocations, got %zu\n", before, liveAllocations); + Require(liveAllocations == before, "copy leaked allocations"); + failures += !succeeded; + CheckValue(objectValue, objectJson); + CheckValue(arrayValue, arrayJson); + } + Require(succeeded, "copy never succeeded"); + } + _ZN3sce4Json6StringD1Ev(&text); + _ZN3sce4Json5ValueD1Ev(&arrayValue); + _ZN3sce4Json5ValueD1Ev(&objectValue); + Require(liveAllocations == initial, "source cleanup leaked allocations"); + std::printf("JSON copy cleanup: %zu operations, %u allocation failures, no leaks\n", sizeof(cases) / sizeof(cases[0]), failures); +} diff --git a/core/libs/tests/Mspace.cpp b/core/libs/tests/Mspace.cpp index 2224e2b7a1..0741744299 100644 --- a/core/libs/tests/Mspace.cpp +++ b/core/libs/tests/Mspace.cpp @@ -30,6 +30,8 @@ void APS5_VABI sceLibcPafMspaceFree_nid_postfix(void*, void*, const void*); void* APS5_VABI sceLibcPafMspaceCalloc_nid_postfix(void*, std::size_t, std::size_t, const void*); void* APS5_VABI sceLibcPafMspaceRealloc_nid_postfix(void*, void*, std::size_t, const void*); void* APS5_VABI sceLibcPafMspaceReallocalign_nid_postfix(void*, void*, std::size_t, std::size_t, const void*); +void* APS5_VABI sceLibcPafMspaceMemalign_nid_postfix(void*, std::size_t, std::size_t, const void*); +int APS5_VABI sceLibcPafMspacePosixMemalign_nid_postfix(void*, void**, std::size_t, std::size_t, const void*); int APS5_VABI sceLibcPafMspaceMallocStats_nid_postfix(void*, void*); int APS5_VABI sceLibcPafMspaceMallocStatsFast_nid_postfix(void*, void*); } @@ -64,6 +66,15 @@ static void CheckPaf() { Require(block != nullptr && block[0] == 0x5a && block[39] == 0x5a); block = static_cast(sceLibcPafMspaceReallocalign_nid_postfix(arena, block, 256, 64, &owner)); Require(block != nullptr && reinterpret_cast(block) % 256 == 0 && block[39] == 0x5a); + auto* aligned = static_cast(sceLibcPafMspaceMemalign_nid_postfix(arena, 128, 50, &owner)); + Require(aligned != nullptr && reinterpret_cast(aligned) % 128 == 0); + Require(sceLibcPafMspaceMemalign_nid_postfix(arena, 0, 50, &owner) == nullptr && *__error_nid_postfix() == 22); + Require(sceLibcPafMspaceMemalign_nid_postfix(arena, 15, 50, &owner) == nullptr && *__error_nid_postfix() == 22); + void* posixAligned = nullptr; + Require(sceLibcPafMspacePosixMemalign_nid_postfix(arena, &posixAligned, 256, 60, &owner) == 0); + Require(posixAligned != nullptr && reinterpret_cast(posixAligned) % 256 == 0); + Require(sceLibcPafMspacePosixMemalign_nid_postfix(arena, nullptr, 256, 60, &owner) == 22); + Require(sceLibcPafMspacePosixMemalign_nid_postfix(arena, &posixAligned, 15, 60, &owner) == 22); MallocManagedSize viaPaf{}, viaMspace{}, fastPaf{}, fastMspace{}; viaPaf.size = viaMspace.size = fastPaf.size = fastMspace.size = sizeof(MallocManagedSize); viaPaf.version = viaMspace.version = fastPaf.version = fastMspace.version = 1; @@ -73,6 +84,8 @@ static void CheckPaf() { Require(fastPaf.currentInuseSize == fastMspace.currentInuseSize); sceLibcPafMspaceFree_nid_postfix(arena, block, &owner); sceLibcPafMspaceFree_nid_postfix(arena, zeroed, arena); + sceLibcPafMspaceFree_nid_postfix(arena, aligned, &owner); + sceLibcPafMspaceFree_nid_postfix(arena, posixAligned, &owner); sceLibcPafMspaceFree_nid_postfix(arena, nullptr, &owner); Require(sceLibcPafMspaceDestroy_nid_postfix(arena) == 0); Require(sceLibcPafMspaceDestroy_nid_postfix(arena) == -1); diff --git a/core/libs/tests/NetBlockingSend.cpp b/core/libs/tests/NetBlockingSend.cpp new file mode 100644 index 0000000000..fb64d834ba --- /dev/null +++ b/core/libs/tests/NetBlockingSend.cpp @@ -0,0 +1,146 @@ +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +static int InterceptSend(SOCKET socket, const char* buffer, int length, int flags); +static int InterceptSendto(SOCKET socket, const char* buffer, int length, int flags, const sockaddr* address, int addressLength); +#define send(...) InterceptSend(__VA_ARGS__) +#define sendto(...) InterceptSendto(__VA_ARGS__) +#endif +#include "../prx/libSceNet/Export.cpp" +#ifdef _WIN32 +#undef send +#undef sendto +#endif +#include +#include +#include + +#ifdef _WIN32 +static int InterceptSend(SOCKET socket, const char* buffer, int length, int flags) { + return ::send(socket, buffer, std::min(length, 16384), flags); +} +static int InterceptSendto(SOCKET socket, const char* buffer, int length, int flags, const sockaddr* address, int addressLength) { + return ::sendto(socket, buffer, std::min(length, 16384), flags, address, addressLength); +} +#endif + +static void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "blocking send: %s\n", message); + std::exit(1); +} + +static void SetOption(int descriptor, int option, int value) { + Require(sceNetSetsockopt(descriptor, NET_SOL_SOCKET, option, &value, sizeof(value)) == 0, "set socket option"); +} + +struct Connection { + int listener = sceNetSocket("blocking-listener", 2, 1, 0); + int sender = sceNetSocket("blocking-sender", 2, 1, 0); + int receiver = -1; + + Connection() { + Require(listener >= 0 && sender >= 0, "create TCP sockets"); + SetOption(listener, NET_SO_RCVBUF, 4096); + SetOption(sender, NET_SO_SNDBUF, 4096); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(sceNetBind_nid_postfix(listener, address.data(), address.size()) == 0, "bind listener"); + std::uint32_t size = address.size(); + Require(sceNetGetsockname(listener, address.data(), &size) == 0, "get listener address"); + Require(sceNetListen(listener, 1) == 0, "listen"); + Require(sceNetConnect(sender, address.data(), size) == 0, "connect"); + receiver = sceNetAccept(listener, nullptr, nullptr); + Require(receiver >= 0, "accept"); + SetOption(receiver, NET_SO_RCVBUF, 1024 * 1024); + SetOption(receiver, 0x1106, 2000000); + } + + ~Connection() { + for (const int descriptor : {receiver, sender, listener}) + Require(sceNetSocketClose(descriptor) == 0, "close socket"); + } +}; + +static std::int64_t Send(std::string_view api, int descriptor, std::vector& bytes, int flags = 0) { + if (api == "send") return sceNetSend(descriptor, bytes.data(), bytes.size(), flags); + std::array address{}; + std::uint32_t size = address.size(); + Require(sceNetGetpeername(descriptor, address.data(), &size) == 0, "get destination address"); + if (api == "sendto") return sceNetSendto(descriptor, bytes.data(), bytes.size(), flags, address.data(), size); + Require(api == "sendmsg", "unknown API"); + std::array vectors{{{bytes.data(), bytes.size() / 2}, + {bytes.data() + bytes.size() / 2, bytes.size() - bytes.size() / 2}}}; + const NetMsghdr message{address.data(), size, vectors.data(), static_cast(vectors.size()), nullptr, 0, 0}; + return sceNetSendmsg(descriptor, &message, flags); +} + +int main(int argc, char** argv) { + Require(argc == 2, "one API argument required"); + const std::string_view api(argv[1]); + std::vector bytes(4 * 1024 * 1024); + for (std::size_t index = 0; index < bytes.size(); ++index) + bytes[index] = static_cast((index * 31 + index / 65536) % 256); + { + Connection connection; + auto sending = std::async(std::launch::async, [&] { return Send(api, connection.sender, bytes); }); + const auto early = sending.wait_for(std::chrono::milliseconds(50)); +#ifdef _WIN32 + if (early == std::future_status::ready) { + std::fprintf(stderr, "early send returned %lld of %zu bytes\n", static_cast(sending.get()), bytes.size()); + Require(false, "send returned before the peer drained its buffer"); + } +#else + static_cast(early); +#endif + std::array buffer{}; + std::size_t received = 0; + while (received < bytes.size()) { + const auto count = sceNetRecv(connection.receiver, buffer.data(), std::min(buffer.size(), bytes.size() - received), 0); + Require(count > 0, "send did not deliver the whole payload"); + Require(std::equal(buffer.begin(), buffer.begin() + count, bytes.begin() + received), "send changed the payload"); + received += static_cast(count); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + Require(sending.wait_for(std::chrono::seconds(2)) == std::future_status::ready, "send did not complete"); + Require(sending.get() == static_cast(bytes.size()), "send returned a short count"); + std::printf("%.*s: sent=%zu, delivered=%zu\n", static_cast(api.size()), api.data(), bytes.size(), received); + } + { + Connection connection; + SetOption(connection.sender, 0x1105, 100000); + auto started = std::chrono::steady_clock::now(); + const auto partial = Send(api, connection.sender, bytes); + Require(partial > 0 && partial < static_cast(bytes.size()), "timeout must return queued bytes"); + Require(std::chrono::steady_clock::now() - started >= std::chrono::milliseconds(80), "partial send returned before its timeout"); + bool emptyTimeout = false; + for (int attempt = 0; attempt < 16; ++attempt) { + const auto result = Send(api, connection.sender, bytes); + if (result < 0) { + Require(*sceNetErrnoLoc() == NET_EAGAIN, "timeout without progress must return EAGAIN"); + emptyTimeout = true; + break; + } + Require(result > 0 && result < static_cast(bytes.size()), "invalid timed byte count"); + } + Require(emptyTimeout, "peer never exhausted the send buffers"); + } + { + Connection connection; + auto sending = std::async(std::launch::async, [&] { return Send(api, connection.sender, bytes); }); + Require(sending.wait_for(std::chrono::milliseconds(50)) == std::future_status::timeout, "send did not wait before abort"); + Require(sceNetSocketAbort(connection.sender, 0) == 0, "abort a blocked send"); + Require(sending.wait_for(std::chrono::seconds(2)) == std::future_status::ready, "abort did not wake a blocked send"); + const auto partial = sending.get(); + Require(partial > 0 && partial < static_cast(bytes.size()), "error discarded queued bytes"); + } + { + Connection connection; + SetOption(connection.sender, NET_SO_NBIO, 1); + const auto partial = Send(api, connection.sender, bytes); + Require(partial > 0 && partial <= static_cast(bytes.size()), "invalid guest nonblocking send result"); +#ifdef _WIN32 + Require(partial <= 16384, "guest nonblocking send did not return its first host result"); +#endif + } + std::puts("timeout, abort and guest nonblocking checks passed"); +} diff --git a/core/libs/tests/NetDontWait.cpp b/core/libs/tests/NetDontWait.cpp new file mode 100644 index 0000000000..7507f2fde5 --- /dev/null +++ b/core/libs/tests/NetDontWait.cpp @@ -0,0 +1,239 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct NetIovec { + void* base; + std::uint64_t length; +}; + +struct NetMsghdr { + void* name; + std::uint32_t nameLength; + NetIovec* iov; + int iovLength; + void* control; + std::uint32_t controlLength; + int flags; +}; + +extern "C" { +int APS5_VABI sceNetSocket(const char*, int, int, int); +int APS5_VABI sceNetSocketClose(int); +int APS5_VABI sceNetSocketAbort(int, int); +int APS5_VABI sceNetBind_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI sceNetGetsockname(int, void*, std::uint32_t*); +int APS5_VABI sceNetListen(int, int); +int APS5_VABI sceNetConnect(int, const void*, std::uint32_t); +int APS5_VABI sceNetAccept(int, void*, std::uint32_t*); +int APS5_VABI sceNetSetsockopt(int, int, int, const void*, std::uint32_t); +int APS5_VABI sceNetGetsockopt(int, int, int, void*, std::uint32_t*); +std::int64_t APS5_VABI sceNetSend(int, const void*, std::size_t, int); +std::int64_t APS5_VABI sceNetSendto(int, const void*, std::size_t, int, const void*, std::uint32_t); +std::int64_t APS5_VABI sceNetSendmsg(int, const NetMsghdr*, int); +std::int64_t APS5_VABI sceNetRecv(int, void*, std::size_t, int); +std::int64_t APS5_VABI sceNetRecvfrom(int, void*, std::size_t, int, void*, std::uint32_t*); +std::int64_t APS5_VABI sceNetRecvmsg(int, NetMsghdr*, int); +int* APS5_VABI sceNetErrnoLoc(); +int APS5_VABI fcntl_nid_postfix(int, int, ...); +} + +namespace { + +constexpr int DontWait = 0x80; + +void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "net dontwait: %s\n", message); + std::exit(1); +} + +void SetOption(int descriptor, int option, int value) { + Require(sceNetSetsockopt(descriptor, 0xffff, option, &value, sizeof(value)) == 0, "set socket option"); +} + +struct Connection { + int listener = sceNetSocket("dontwait-listener", 2, 1, 0); + int sender = sceNetSocket("dontwait-sender", 2, 1, 0); + int receiver = -1; + + Connection() { + Require(listener >= 0 && sender >= 0, "create TCP sockets"); + SetOption(listener, 0x1002, 4096); + SetOption(sender, 0x1001, 4096); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(sceNetBind_nid_postfix(listener, address.data(), address.size()) == 0, "bind listener"); + std::uint32_t size = address.size(); + Require(sceNetGetsockname(listener, address.data(), &size) == 0, "get listener address"); + Require(sceNetListen(listener, 1) == 0, "listen"); + Require(sceNetConnect(sender, address.data(), size) == 0, "blocking connect"); + receiver = sceNetAccept(listener, nullptr, nullptr); + Require(receiver >= 0, "blocking accept"); + SetOption(receiver, 0x1002, 4096); + SetOption(receiver, 0x1106, 2000000); + } + + ~Connection() { + for (const int descriptor : {receiver, sender, listener}) + Require(sceNetSocketClose(descriptor) == 0, "close socket"); + } + + void Abort() { + sceNetSocketAbort(sender, 0); + sceNetSocketAbort(receiver, 0); + } +}; + +std::int64_t Send(std::string_view api, int descriptor, const std::vector& bytes) { + if (api == "send" || api == "duplex") return sceNetSend(descriptor, bytes.data(), bytes.size(), DontWait); + if (api == "sendto") return sceNetSendto(descriptor, bytes.data(), bytes.size(), DontWait, nullptr, 0); + std::array vectors{{{const_cast(bytes.data()), bytes.size() / 2}, + {const_cast(bytes.data()) + bytes.size() / 2, bytes.size() - bytes.size() / 2}}}; + const NetMsghdr message{nullptr, 0, vectors.data(), static_cast(vectors.size()), nullptr, 0, 0}; + return sceNetSendmsg(descriptor, &message, DontWait); +} + +void CheckSend(std::string_view api) { + Connection connection; + std::vector bytes(16 * 1024 * 1024, 'x'); + std::future reading; + char reply = 0; + if (api == "duplex") { + reading = std::async(std::launch::async, [&] { return sceNetRecv(connection.sender, &reply, 1, 0); }); + Require(reading.wait_for(std::chrono::milliseconds(50)) == std::future_status::timeout, + "ordinary receive must wait while a DONTWAIT send runs on the same socket"); + } + auto sending = std::async(std::launch::async, [&] { return Send(api, connection.sender, bytes); }); + const bool completed = sending.wait_for(std::chrono::milliseconds(500)) == std::future_status::ready; + if (!completed) connection.Abort(); + const auto sent = sending.get(); + Require(completed, "MSG_DONTWAIT send blocked behind a slow peer or an ordinary receive"); + Require(sent > 0 && sent <= static_cast(bytes.size()), "large send returned an invalid byte count"); + int nonblocking = -1; + std::uint32_t size = sizeof(nonblocking); + Require(sceNetGetsockopt(connection.sender, 0xffff, 0x1200, &nonblocking, &size) == 0 && nonblocking == 0, + "DONTWAIT changed the socket option"); + Require(fcntl_nid_postfix(connection.sender, 3) == 2, "DONTWAIT changed descriptor flags"); + if (reading.valid()) { + Require(reading.wait_for(std::chrono::milliseconds(0)) == std::future_status::timeout, + "DONTWAIT interrupted an ordinary receive"); + const char value = 'r'; + Require(sceNetSend(connection.receiver, &value, 1, 0) == 1, "send reply"); + if (reading.wait_for(std::chrono::seconds(2)) != std::future_status::ready) connection.Abort(); + Require(reading.get() == 1 && reply == value, "ordinary receive did not complete with its reply"); + } + std::int64_t received = 0; + SetOption(connection.receiver, 0x1002, 1024 * 1024); + std::array buffer{}; + while (received < sent) { + const auto count = sceNetRecv(connection.receiver, buffer.data(), + std::min(buffer.size(), static_cast(sent - received)), 0); + Require(count > 0, "reported send bytes were not delivered"); + for (std::int64_t index = 0; index < count; ++index) Require(buffer[index] == 'x', "payload changed"); + received += count; + } + Require(sceNetRecv(connection.receiver, buffer.data(), buffer.size(), DontWait) < 0 && *sceNetErrnoLoc() == 35, + "send left unreported bytes or receive did not return EAGAIN"); + std::printf("%.*s: sent=%lld, delivered=%lld, blocking flags preserved\n", + static_cast(api.size()), api.data(), static_cast(sent), static_cast(received)); +} + +std::pair Receive(std::string_view api, int descriptor) { + char byte = 0; + std::int64_t result; + if (api == "recv") result = sceNetRecv(descriptor, &byte, 1, DontWait); + else if (api == "recvfrom") result = sceNetRecvfrom(descriptor, &byte, 1, DontWait, nullptr, nullptr); + else { + NetIovec vector{&byte, 1}; + NetMsghdr message{nullptr, 0, &vector, 1, nullptr, 0, 0}; + result = sceNetRecvmsg(descriptor, &message, DontWait); + } + if (result == 1) Require(byte == 'x', "received payload changed"); + return {result, *sceNetErrnoLoc()}; +} + +void CheckReceive(std::string_view api) { + Connection connection; + SetOption(connection.receiver, 0x1106, 0); + constexpr int readers = 8; + for (int iteration = 0; iteration < 32; ++iteration) { + const char byte = 'x'; + Require(sceNetSend(connection.sender, &byte, 1, 0) == 1, "send race payload"); + std::barrier start(readers + 1); + std::vector>> receiving; + for (int index = 0; index < readers; ++index) + receiving.push_back(std::async(std::launch::async, [&] { + start.arrive_and_wait(); + return Receive(api, connection.receiver); + })); + start.arrive_and_wait(); + int successes = 0; + for (auto& future : receiving) { + const bool completed = future.wait_for(std::chrono::milliseconds(500)) == std::future_status::ready; + if (!completed) connection.Abort(); + const auto [result, error] = future.get(); + Require(completed, "MSG_DONTWAIT receive blocked after another reader consumed the byte"); + if (result == 1) ++successes; + else Require(result < 0 && error == 35, "empty receive must return EAGAIN"); + } + Require(successes == 1, "one byte must be delivered to exactly one reader"); + } + std::printf("%.*s: 32 races, one successful reader and seven EAGAIN results each\n", + static_cast(api.size()), api.data()); +} + +void CheckBlocking() { + const int listener = sceNetSocket("blocking-listener", 2, 1, 0); + const int sender = sceNetSocket("blocking-sender", 2, 1, 0); + Require(listener >= 0 && sender >= 0, "create blocking sockets"); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(sceNetBind_nid_postfix(listener, address.data(), address.size()) == 0, "bind blocking listener"); + std::uint32_t size = address.size(); + Require(sceNetGetsockname(listener, address.data(), &size) == 0, "get blocking listener address"); + Require(sceNetListen(listener, 1) == 0, "listen before accept"); + auto accepting = std::async(std::launch::async, [&] { return sceNetAccept(listener, nullptr, nullptr); }); + Require(accepting.wait_for(std::chrono::milliseconds(50)) == std::future_status::timeout, "accept did not wait for a connection"); + Require(sceNetConnect(sender, address.data(), size) == 0, "connect to waiting accept"); + if (accepting.wait_for(std::chrono::seconds(2)) != std::future_status::ready) sceNetSocketAbort(listener, 0); + const int receiver = accepting.get(); + Require(receiver >= 0, "waiting accept failed"); + SetOption(receiver, 0x1200, 1); + char byte = 0; + Require(sceNetRecv(receiver, &byte, 1, 0) < 0 && *sceNetErrnoLoc() == 35, "socket nonblocking option did not affect receive"); + SetOption(receiver, 0x1200, 0); + auto receiving = std::async(std::launch::async, [&] { return sceNetRecv(receiver, &byte, 1, 0); }); + Require(receiving.wait_for(std::chrono::milliseconds(50)) == std::future_status::timeout, "blocking receive did not wait"); + Require(sceNetSocketClose(receiver) == 0, "close a socket with a waiting receive"); + Require(receiving.wait_for(std::chrono::seconds(2)) == std::future_status::ready, "close did not wake the receive"); + const auto closedResult = receiving.get(); + std::fprintf(stderr, "closed receive result=%lld\n", static_cast(closedResult)); + Require(closedResult <= 0, "closed receive returned data that was never sent"); + Require(sceNetSocketClose(sender) == 0 && sceNetSocketClose(listener) == 0, "close blocking fixture"); + std::puts("blocking: accept waited, socket mode toggled, close woke the pending receive"); +} + +} + +int main(int argc, char** argv) { + Require(argc == 2, "one API argument required"); + const std::string_view api(argv[1]); + if (api == "blocking") { + CheckBlocking(); + return 0; + } + if (api == "send" || api == "sendto" || api == "sendmsg" || api == "duplex") CheckSend(api); + else { + Require(api == "recv" || api == "recvfrom" || api == "recvmsg", "unknown API"); + CheckReceive(api); + } +} 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/ScalarCall.cpp b/core/libs/tests/ScalarCall.cpp index 4dae722b17..c4d38b87c4 100644 --- a/core/libs/tests/ScalarCall.cpp +++ b/core/libs/tests/ScalarCall.cpp @@ -292,6 +292,83 @@ static void CheckTableCalls() { }); } +enum class HoistedShape { InLoop, Hoisted, SecondWriterOnArm, LoadOnArmOnly, WriterAfterCall, WriterAfterLoop, CallOnArm, ConstantPointer, SkipsNullTarget, LongBranchWriter }; + +static std::vector HoistedTableCaller(HoistedShape shape) { + std::vector code; + const auto descriptorLoad = [&] { code.insert(code.end(), {0xf4081800u, 0xfa000010u, 0xbf8cc07fu}); }; + if (shape == HoistedShape::ConstantPointer) code.insert(code.end(), {0xbeea03ffu, 0xe0040000u, 0xbeeb038fu, 0xf4081835u, 0xfa000000u, 0xbf8cc07fu}); + else if (shape != HoistedShape::InLoop && shape != HoistedShape::LoadOnArmOnly) descriptorLoad(); + code.push_back(0xbf068002u); + code.push_back(shape == HoistedShape::LoadOnArmOnly ? 0xbf850003u : 0xbf850001u); + if (shape == HoistedShape::LoadOnArmOnly) descriptorLoad(); + else code.push_back(shape == HoistedShape::SecondWriterOnArm ? 0xbee00380u : shape == HoistedShape::CallOnArm ? 0xbb1e0000u : shape == HoistedShape::LongBranchWriter ? 0xbf820000u : 0x7e020280u); + const auto arm = code.size() - 1u; + code.push_back(0xbee5037eu); + const auto loop = code.size(); + code.push_back(0xbee4037eu); + if (shape == HoistedShape::InLoop) descriptorLoad(); + code.insert(code.end(), {0x7ed40500u, 0x7da4006au, 0x8f6a846au, 0xf4280430u, 0xd4000000u, 0xbf8cc07fu, 0xbe8e0310u, 0xbe8f0311u, 0xbe900312u, 0xbe910313u}); + if (shape == HoistedShape::SkipsNullTarget) code.insert(code.end(), {0xbf130e80u, 0xbf840001u}); + code.push_back(0xbe8e210eu); + if (shape == HoistedShape::WriterAfterCall) code.push_back(0xbee10301u); + code.push_back(0x897e7e64u); + code.push_back(0xbf890000u | ((static_cast(loop) - static_cast(code.size()) - 1u) & 0xffffu)); + code.push_back(0xbefe0365u); + if (shape == HoistedShape::WriterAfterLoop) code.push_back(0xbee00380u); + code.push_back(0xbf810000u); + if (shape == HoistedShape::CallOnArm) { + code[arm] |= static_cast(code.size() - arm - 1u); + code.insert(code.end(), {0xbe80201eu, 0xbf9f0000u}); + } + if (shape == HoistedShape::LongBranchWriter) { + code[arm] |= static_cast(code.size() - arm - 1u); + const auto getpc = code.size() + 3u; + code.insert(code.end(), {0xf4081800u, 0xfa000020u, 0xbf8cc07fu, 0xbe841f00u, 0x8084ff04u, static_cast((getpc - arm) * 4u), 0xbe802004u, 0xbf9f0000u}); + } + return code; +} + +static void CheckHoistedTableCalls() { + const std::vector callee{0x4a0202c0u, 0xbe80200eu}; + const std::vector> callees{{{0x1122334455ull, callee}}}; + for (const auto shape : {HoistedShape::InLoop, HoistedShape::Hoisted, HoistedShape::SecondWriterOnArm, HoistedShape::LoadOnArmOnly, HoistedShape::WriterAfterCall, HoistedShape::WriterAfterLoop, HoistedShape::CallOnArm, + HoistedShape::ConstantPointer, HoistedShape::SkipsNullTarget, HoistedShape::LongBranchWriter}) { + const bool linkable = shape == HoistedShape::InLoop || shape == HoistedShape::Hoisted || shape == HoistedShape::WriterAfterLoop || shape == HoistedShape::ConstantPointer || + shape == HoistedShape::SkipsNullTarget; + for (const std::uint32_t waveSize : {32u, 64u}) { + const auto code = HoistedTableCaller(shape); + const auto program = RdnaInstructionDecoder{}.Decode(code); + SwappcInfo swappc{false, 0u, 16u, waveSize}; + const auto calls = FindTableCalls(program, swappc); + if (!linkable) { + Require(calls.empty()); + ExpectThrow("not statically resolvable", [&] { static_cast(BuildCode(code, swappc)); }); + continue; + } + const bool constant = shape == HoistedShape::ConstantPointer; + Require(calls.size() == 1u && calls[0].userDataRegister == 0u && calls[0].descriptorOffset == (constant ? 0u : 0x10u) && calls[0].entryOffset == 0u && calls[0].entryStride == 16u); + Require(calls[0].constantPointer == (constant ? std::optional(0xfe0040000ull) : std::nullopt)); + const auto linked = LinkCalls(program, {}, {}, calls, callees); + swappc.linkedCallFallbacks = linked.fallbacks; + static_cast(TranslateCode(linked.code, swappc)); + } + } + const std::vector calledEntry{ + 0xf4081800u, 0xfa000000u, 0xbf8cc07fu, 0xbb1e0001u, 0xbf810000u, 0xbf840003u, 0xf4081800u, 0xfa000010u, + 0xbf8cc07fu, 0xf4280430u, 0xfa000000u, 0xbf8cc07fu, 0xbe8e0310u, 0xbe8f0311u, 0xbe8e210eu, 0xbe80201eu, 0xbf9f0000u}; + Require(FindTableCalls(RdnaInstructionDecoder{}.Decode(calledEntry), SwappcInfo{false, 0u, 16u}).empty()); + const std::vector fallsIntoEntry{ + 0xbf840002u, 0xbb1e0003u, 0xbf810000u, 0xf4081800u, 0xfa000010u, 0xbf8cc07fu, 0xf4280430u, 0xfa000000u, + 0xbf8cc07fu, 0xbe8e0310u, 0xbe8f0311u, 0xbe8e210eu, 0xbe80201eu, 0xbf9f0000u}; + Require(FindTableCalls(RdnaInstructionDecoder{}.Decode(fallsIntoEntry), SwappcInfo{false, 0u, 16u}).empty()); + const std::vector callBetween{ + 0xf4081800u, 0xfa000010u, 0xbf8cc07fu, 0xbb1e0007u, 0xf4280430u, 0xfa000000u, 0xbf8cc07fu, 0xbe8e0310u, + 0xbe8f0311u, 0xbe8e210eu, 0xbf810000u, 0xbe80201eu, 0xbf9f0000u}; + const auto between = FindTableCalls(RdnaInstructionDecoder{}.Decode(callBetween), SwappcInfo{false, 0u, 16u}); + Require(between.size() == 1u && between[0].descriptorOffset == 0x10u); +} + static void CheckRejections() { ExpectThrow("invalid instruction boundary", [] { Build({Call(0u, 8u, 16u), Sopp(4u, 1u)}); @@ -392,6 +469,7 @@ int main(int argc, char** argv) { CheckRejections(); CheckUserDataCalls(); CheckTableCalls(); + CheckHoistedTableCalls(); CheckGuards(""); } else { Require(argc == 2); 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/libs/tests/WindowsMappingsRelease.cpp b/core/libs/tests/WindowsMappingsRelease.cpp new file mode 100644 index 0000000000..6b2f016c97 --- /dev/null +++ b/core/libs/tests/WindowsMappingsRelease.cpp @@ -0,0 +1,81 @@ +#include "prx/libc/include/WindowsMappings.hpp" +#include +#include +#include + +static void Require(bool value, std::source_location location = std::source_location::current()) { + if (!value) throw std::runtime_error("Windows resident release regression at " + std::to_string(location.line())); +} + +static void CheckMetadataMismatch(unsigned mode) { + constexpr std::size_t page = 65536; + auto& mappings = GuestArena::WindowsMappings::Get(); + auto* base = static_cast(mappings.Reserve(nullptr, 3 * page)); + Require(base != nullptr); + const auto section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0, 3 * page, nullptr); + Require(section != nullptr); + auto* address = base + page; + mappings.Map(address, page, section, 0, PAGE_READWRITE, true, true); + Require(VirtualFree(address, 0, MEM_RELEASE)); + if (mode == 0) { + Require(VirtualFree(base, 0, MEM_RELEASE)); + Require(VirtualFree(base + 2 * page, 0, MEM_RELEASE)); + Require(VirtualAlloc(base, 3 * page, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE) == base); + } else if (mode == 1) { + Require(VirtualFree(base + 2 * page, 0, MEM_RELEASE)); + Require(VirtualAlloc(address, 2 * page, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE) == address); + } else { + Require(MapViewOfFileEx(section, FILE_MAP_ALL_ACCESS, 0, 0, page, address) == address); + } + address[0] = 73; + bool rejected = false; + try { mappings.Reset(address, page); } + catch (const std::runtime_error&) { rejected = true; } + std::uint32_t protection = 0; + Require(rejected && address[0] == 73 && mappings.Protection(reinterpret_cast(address), &protection)); + if (mode == 2) Require(UnmapViewOfFile(address)); + else Require(VirtualFree(mode == 0 ? base : address, 0, MEM_RELEASE)); + CloseHandle(section); +} + +static void CheckValidRelease() { + constexpr std::size_t bytes = 2 * 1024 * 1024; + auto& mappings = GuestArena::WindowsMappings::Get(); + auto* reserved = static_cast(mappings.Reserve(nullptr, 2 * bytes)); + Require(reserved != nullptr); + const auto start = (reinterpret_cast(reserved) + bytes - 1) & ~static_cast(bytes - 1); + auto* base = reinterpret_cast(start); + const auto oneRun = [&] { + std::uintptr_t first = 0; + std::uintptr_t last = 0; + return mappings.ResidentSpan(start + bytes - 1, 1, &first, &last) && first == start && last == start + bytes; + }; + const auto section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0, bytes, nullptr); + Require(section != nullptr); + mappings.Map(base, bytes, section, 0, PAGE_READWRITE, true, true); + Require(oneRun()); + base[0] = 31; + base[bytes - 1] = 89; + DWORD previous; + Require(VirtualProtect(base + 65536, 65536, PAGE_READONLY, &previous)); + mappings.Reset(base, bytes); + mappings.Map(base, bytes, section, 0, PAGE_READWRITE, true, true); + Require(oneRun()); + Require(base[0] == 31 && base[bytes - 1] == 89); + mappings.Reset(base, bytes); + Require(VirtualFree(reserved, 2 * bytes, MEM_RELEASE | MEM_COALESCE_PLACEHOLDERS)); + Require(VirtualFree(reserved, 0, MEM_RELEASE)); + mappings.ForgetSection(section); + CloseHandle(section); +} + +int main(int argc, char** argv) { + try { + if (argc == 2) CheckMetadataMismatch(static_cast(argv[1][0] - '0')); + else CheckValidRelease(); + } catch (const std::exception& failure) { + std::fprintf(stderr, "%s\n", failure.what()); + return 1; + } + return 0; +} diff --git a/core/libs/tests/guest_arena/CMakeLists.txt b/core/libs/tests/guest_arena/CMakeLists.txt index f84f4e6842..e9a10f6a4c 100644 --- a/core/libs/tests/guest_arena/CMakeLists.txt +++ b/core/libs/tests/guest_arena/CMakeLists.txt @@ -15,7 +15,7 @@ set_target_properties(guest_arena_tests PROPERTIES CXX_EXTENSIONS OFF RUNTIME_OUTPUT_DIRECTORY "$<1:${ANYPS5_TEST_OUTPUT_DIRECTORY}>") -foreach(mode available unmappable host commit) +foreach(mode available unmappable host gap commit) add_test(NAME guest_arena_${mode} COMMAND guest_arena_tests ${mode}) set_tests_properties(guest_arena_${mode} PROPERTIES TIMEOUT 30) endforeach() diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index d97c3d48a8..fa78fbaa42 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -31,6 +31,7 @@ add_library(relinker_core STATIC ${CMAKE_CURRENT_SOURCE_DIR}/io/src/ByteReader.cpp ${CMAKE_CURRENT_SOURCE_DIR}/io/src/BufferUtils.cpp ${CMAKE_CURRENT_SOURCE_DIR}/io/src/NativePath.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/io/src/FileIdentity.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/Amd64OnlyConverter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/InstructionScanner.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/X64InstructionDecoder.cpp @@ -87,6 +88,9 @@ target_include_directories(relinker_core PUBLIC target_compile_features(relinker_core PUBLIC cxx_std_20) set_target_properties(relinker_core PROPERTIES CXX_EXTENSIONS OFF) +if(WIN32) + set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/io/src/FileIdentity.cpp PROPERTIES COMPILE_DEFINITIONS _WIN32_WINNT=0x0602) +endif() if(BUILD_TESTING) add_test_executable(andn_lowering_tests ${CMAKE_CURRENT_SOURCE_DIR}/codegen/tests/AndnLoweringTests.cpp) @@ -296,6 +300,14 @@ if(BUILD_TESTING) target_include_directories(buffer_bounds_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/io/include) add_test(NAME buffer_bounds COMMAND buffer_bounds_tests) + add_test_executable(file_identity_tests + ${CMAKE_CURRENT_SOURCE_DIR}/io/tests/FileIdentityTests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/io/src/FileIdentity.cpp + ) + target_include_directories(file_identity_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/io/include) + add_test(NAME file_identity COMMAND file_identity_tests) + set_tests_properties(file_identity PROPERTIES TIMEOUT 20) + add_test_executable(param_json_utf8_tests ${CMAKE_CURRENT_SOURCE_DIR}/../libs/tests/ParamJsonParser.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp) @@ -454,15 +466,23 @@ if(BUILD_TESTING) add_test(NAME guest_none_relocations COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_none_relocations.py $) set_tests_properties(guest_none_relocations PROPERTIES TIMEOUT 20) + add_test(NAME interpreter_in_code COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_interpreter_in_code.py $) + set_tests_properties(interpreter_in_code PROPERTIES TIMEOUT 20) 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} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_optional_rela.py $) add_test(NAME empty_tls COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_empty_tls.py $) + add_test(NAME windows_tls_zero_fill COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_tls_zero_fill.py $) add_test(NAME windows_tls_protected_entries COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_tls_protected_entries.py $) set(tlsCoverageGroups metadata padding register_loads displacement_bounds alu_execution register_address_loads moved_branch rejected) @@ -472,7 +492,7 @@ if(BUILD_TESTING) foreach(opcode IN ITEMS add or adc sbb and sub xor cmp) list(APPEND tlsCoverageGroups alu_${opcode}) endforeach() - list(APPEND tlsCoverageGroups gs_traps) + list(APPEND tlsCoverageGroups gs_traps gs_traps_implicit) foreach(group IN LISTS tlsCoverageGroups) add_test(NAME tls_function_coverage_${group} COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_tls_function_coverage.py $ ${group}) @@ -480,6 +500,9 @@ if(BUILD_TESTING) endforeach() add_test(NAME tls_diagnostics COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_tls_diagnostics.py $) + add_test(NAME call_zero_padding COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_call_zero_padding.py $) + set_tests_properties(call_zero_padding PROPERTIES TIMEOUT 60) add_test(NAME macos_tls_alu COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_macos_tls_alu.py $) set_tests_properties(macos_tls_alu PROPERTIES TIMEOUT 60) @@ -497,6 +520,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} @@ -514,6 +540,8 @@ if(BUILD_TESTING) set_tests_properties(windows_runpath PROPERTIES TIMEOUT 30) add_test(NAME windows_trace_imports COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_trace_imports.py $) + add_test(NAME linux_trace_imports COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_trace_imports.py $) add_test(NAME windows_address_space COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_address_space.py $) add_test(NAME windows_tls_thread_pointer COMMAND ${Python3_EXECUTABLE} @@ -581,6 +609,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) @@ -592,6 +624,11 @@ if(BUILD_TESTING) add_asan_test_options(guest_module_fini verify_asan_link_order=0) add_test(NAME guest_needed_nested_modules COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_needed_nested_modules.py $) + add_test(NAME guest_zero_initializer COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_zero_initializer.py + $ $) + set_tests_properties(guest_zero_initializer PROPERTIES TIMEOUT 30) + add_asan_test_options(guest_zero_initializer verify_asan_link_order=0) set(windows_stdio_host_arguments) if(WIN32) if(MINGW) @@ -660,6 +697,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} @@ -684,6 +723,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_syscall_number.py $) add_test(NAME unused_filter_indirect COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_unused_filter_indirect.py $) + add_test(NAME unused_filter_fallthrough_padding COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_unused_filter_fallthrough_padding.py $) else() message(STATUS "Python3 interpreter not found; skipping Python relinker tests") endif() @@ -715,6 +756,10 @@ if(BUILD_TESTING) ) add_test(NAME windows_icon_resource COMMAND windows_icon_resource_tests) + add_test_executable(windows_trampoline_builder_tests ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/tests/WindowsTrampolineBuilderTests.cpp) + target_link_libraries(windows_trampoline_builder_tests PRIVATE relinker_core) + add_test(NAME windows_trampoline_builder COMMAND windows_trampoline_builder_tests) + add_test_executable(entry_stub_tests ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/tests/EntryStubTests.cpp) target_link_libraries(entry_stub_tests PRIVATE relinker_core) add_test(NAME entry_stub COMMAND entry_stub_tests) @@ -798,6 +843,9 @@ if(BUILD_TESTING) endif() if(BUILD_TESTING AND Python3_Interpreter_FOUND) + add_test(NAME macos_executable_exports COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_macos_executable_exports.py $) + set_tests_properties(macos_executable_exports PROPERTIES TIMEOUT 30 LABELS relinker) add_test(NAME macos_guest_weak_imports COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_macos_guest_weak_imports.py $) set_property(TEST macos_guest_weak_imports APPEND PROPERTY LABELS relinker) diff --git a/core/relinker/cli/src/CliArgs.cpp b/core/relinker/cli/src/CliArgs.cpp index 7cfe452ea7..3fa915c6d2 100644 --- a/core/relinker/cli/src/CliArgs.cpp +++ b/core/relinker/cli/src/CliArgs.cpp @@ -197,8 +197,8 @@ Args ParseArgs(int argc, char* argv[]) { if (args.windowsFixedBase && !args.toWindows) throw std::runtime_error("--windows-fixed-base requires --windows"); - if (args.traceImports && !args.toWindows) - throw std::runtime_error("--trace-imports requires --windows"); + if (args.traceImports && args.toMacos) + throw std::runtime_error("--trace-imports cannot be used with --macos"); if (args.traceImports && args.lazyBinding) throw std::runtime_error("--trace-imports conflicts with --lazy-binding"); if (args.linuxFixedBase && (args.toWindows || args.toMacos)) diff --git a/core/relinker/cli/src/NativeModules.cpp b/core/relinker/cli/src/NativeModules.cpp index 75f6a899e1..f01ac374d4 100644 --- a/core/relinker/cli/src/NativeModules.cpp +++ b/core/relinker/cli/src/NativeModules.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -64,7 +65,7 @@ NativeModules ReadModules(const std::filesystem::path& inputPath, const std::fil continue; } if (!entry.is_regular_file()) continue; - if (std::filesystem::equivalent(entry.path(), inputPath)) continue; + if (Io::SameFile(entry.path(), inputPath)) continue; bool self = false; if (isElf(entry.path(), self)) paths.push_back(entry.path()); else if (self) @@ -81,7 +82,7 @@ NativeModules ReadModules(const std::filesystem::path& inputPath, const std::fil std::map sonames; const auto soname = [&](std::size_t index) -> const std::string& { const auto [found, inserted] = sonames.try_emplace(index); - if (inserted) found->second = GuestImageReader().Read(result.Modules[index].Path, result.Modules[index].Bytes).Soname; + if (inserted) found->second = GuestImageReader().Read(result.Modules[index].Path, result.Modules[index].Bytes, true).Soname; return found->second; }; std::vector copies(result.Modules.size()); diff --git a/core/relinker/codegen/include/codegen/CodegenTypes.hpp b/core/relinker/codegen/include/codegen/CodegenTypes.hpp index 72bace4267..021f2bf059 100644 --- a/core/relinker/codegen/include/codegen/CodegenTypes.hpp +++ b/core/relinker/codegen/include/codegen/CodegenTypes.hpp @@ -68,6 +68,7 @@ struct DecodedInstructionInfo { bool AddressSize32; std::size_t RipRelativeDispOffset; bool HasModRm; + bool HasMemoryOperand; std::uint8_t ModRmByte; bool HasBranchTarget; std::int64_t BranchDisp; diff --git a/core/relinker/codegen/include/codegen/IInstructionScanner.hpp b/core/relinker/codegen/include/codegen/IInstructionScanner.hpp index 2ea39a4e4f..a09771a326 100644 --- a/core/relinker/codegen/include/codegen/IInstructionScanner.hpp +++ b/core/relinker/codegen/include/codegen/IInstructionScanner.hpp @@ -17,7 +17,9 @@ 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::vector MergeCodeRanges(std::vector ranges); + +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/include/codegen/x86/X64OpcodeConstants.hpp b/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp index 7a9a14a364..c6db778a9e 100644 --- a/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp +++ b/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp @@ -146,6 +146,7 @@ inline constexpr std::uint8_t TwoByteMovdMovq = 0x7E; inline constexpr std::uint8_t TwoByteMovqMmxXmm = 0x6F; inline constexpr std::uint8_t TwoByteMovdqu = 0x7F; inline constexpr std::uint8_t TwoByteGrp7 = 0x01; +inline constexpr std::uint8_t TwoByteMaskmov = 0xF7; inline constexpr std::uint8_t TwoByteShiftImm8Min = 0x70; inline constexpr std::uint8_t TwoByteShiftImm8Max = 0x73; inline constexpr std::uint8_t ThreeByteEscape38 = 0x38; diff --git a/core/relinker/codegen/src/Amd64OnlyConverter.cpp b/core/relinker/codegen/src/Amd64OnlyConverter.cpp index a2ea235db6..8f312031fe 100644 --- a/core/relinker/codegen/src/Amd64OnlyConverter.cpp +++ b/core/relinker/codegen/src/Amd64OnlyConverter.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -260,6 +261,15 @@ std::set Amd64OnlyConverter::_collectBranchTargets( const auto match = std::lower_bound(matches.begin(), matches.end(), offset, [](const InstructionMatch& item, const std::uint64_t value) { return item.Offset < value; }); return match != matches.end() && match->Offset == offset; }; + const auto pastPrefixes = [&](const std::uint64_t address) { + const auto offset = address - ph.MappedAddress; + const auto after = std::upper_bound(matches.begin(), matches.end(), offset, [](const std::uint64_t value, const InstructionMatch& item) { return value < item.Offset; }); + if (after == matches.begin()) + return false; + const auto& match = *std::prev(after); + const auto inside = offset - match.Offset; + return inside != 0 && inside < match.Length && inside > DecodedInstruction{seg.data() + match.Offset, match.Length}.OpcodeOffset(); + }; const X64InstructionDecoder decoder; std::set targets; for (const auto& match : matches) { @@ -267,7 +277,7 @@ std::set Amd64OnlyConverter::_collectBranchTargets( const auto next = static_cast(ph.MappedAddress + match.Offset + info.Length); if (info.HasBranchTarget && !info.HasRipRelativeDisp) { const auto target = next + info.BranchDisp; - if (target >= 0) + if (target >= 0 && !pastPrefixes(static_cast(target))) targets.insert(static_cast(target)); } else if (info.HasRipRelativeDisp && info.Opcode == kLea) { std::int32_t displacement; @@ -386,6 +396,8 @@ void Amd64OnlyConverter::_convertSegment( for (auto next = item.Index + 1; siteLength < Amd64OnlySubstitutionTable::kJmpRel32.Size; ++next) { if (next >= matches.size() || matches[next].Offset != match.Offset + siteLength) throw CodegenException("AMD-only instruction too short for a jump and not followed by an instruction", fileOffset); + if (claimed.contains(next)) + throw CodegenException("AMD-only instruction too short for a jump is followed by an instruction another stub already moved", ph.Offset + matches[next].Offset); const auto& following = matches[next]; const std::span bytes(seg.data() + following.Offset, following.Length); const auto info = decoder.DecodeInstruction(bytes.data(), bytes.size()); diff --git a/core/relinker/codegen/src/InstructionScanner.cpp b/core/relinker/codegen/src/InstructionScanner.cpp index 328e1e532b..28f8544994 100644 --- a/core/relinker/codegen/src/InstructionScanner.cpp +++ b/core/relinker/codegen/src/InstructionScanner.cpp @@ -8,28 +8,46 @@ namespace Codegen { +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 +56,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 +81,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..739a3aea7f 100644 --- a/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp +++ b/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -108,6 +109,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,8 +189,11 @@ 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; + [[nodiscard]] const char* _emitInPlace(StubBodyBuilder& body, const DecodedInstruction& instr) const; }; Amd64OnlyMatch Amd64OnlyInstructionMatcher::_matchMovnts(const DecodedInstruction& instr, const Entry& entry) const { @@ -222,6 +234,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 +293,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; @@ -292,6 +328,33 @@ const char* Amd64OnlyInstructionMatcher::_emitOutOfLine(StubBodyBuilder& body, c return nullptr; } +const char* Amd64OnlyInstructionMatcher::_emitInPlace(StubBodyBuilder& body, const DecodedInstruction& instr) const { + if (const auto inPlace = Match(instr.Data, instr.Length); inPlace && inPlace->Lowering == Amd64OnlyLowering::InPlace) { + for (const auto* entry : {&kMovntss, &kMovntsd, &kMonitorx, &kMwaitx, &kRdseed, &kClwb}) { + if (inPlace->InstructionName != entry->Name) + continue; + const X64InstructionDecoder decoder; + const std::span replacement(inPlace->ReplacementBytes); + for (std::size_t offset = 0; offset < replacement.size();) { + const auto info = decoder.DecodeInstruction(replacement.data() + offset, replacement.size() - offset); + if (info.Length == 0 || info.Length > replacement.size() - offset) + throw CodegenException("In-place replacement does not split into whole instructions"); + const auto part = replacement.subspan(offset, info.Length); + if (info.HasRipRelativeDisp) { + std::int32_t displacement = 0; + std::memcpy(&displacement, part.data() + info.RipRelativeDispOffset, sizeof(displacement)); + body.RipRelative(part, info.RipRelativeDispOffset, static_cast(offset + info.Length) + displacement); + } else { + body.Raw(part); + } + offset += info.Length; + } + return entry->Name; + } + } + return nullptr; +} + void Amd64OnlyInstructionMatcher::EmitOutOfLine(StubBodyBuilder& body, const std::span instruction) const { if (_emitOutOfLine(body, DecodedInstruction{instruction.data(), instruction.size()}) == nullptr) throw CodegenException("AMD-only instruction has no out-of-line lowering"); @@ -308,7 +371,9 @@ std::optional Amd64OnlyInstructionMatcher::MatchSequence( const char* name = nullptr; for (const auto& instruction : instructions) { const DecodedInstruction instr{instruction.data(), instruction.size()}; - const auto emitted = _emitOutOfLine(body, instr); + auto emitted = _emitOutOfLine(body, instr); + if (emitted == nullptr && name != nullptr) + emitted = _emitInPlace(body, instr); if (emitted == nullptr) return std::nullopt; if (name == nullptr) @@ -363,10 +428,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/src/x86/X64InstructionDecoder.cpp b/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp index c6cc9de4ac..b22c3f8726 100644 --- a/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp +++ b/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp @@ -356,6 +356,7 @@ DecodedInstructionInfo X64InstructionDecoder::DecodeInstruction( std::size_t pos = 0; bool operandSizeOverride = false; + bool repPrefix = false; while (pos < info.Length) { std::uint8_t b = data[pos]; @@ -374,6 +375,8 @@ DecodedInstructionInfo X64InstructionDecoder::DecodeInstruction( info.AddressSize32 = true; if (b == PrefixOperandSize) operandSizeOverride = true; + if (b == PrefixRep) + repPrefix = true; info.RexPrefix = 0; ++pos; continue; @@ -393,6 +396,8 @@ DecodedInstructionInfo X64InstructionDecoder::DecodeInstruction( info.ModRmRegField = (modrm >> ModRmRegShift) & ModRmRegMask; const std::uint8_t mod = registerOnly ? ModRmModRegister : (modrm >> ModRmModShift) & ModRmModMask; const std::uint8_t rm = modrm & ModRmRmMask; + if (mod != ModRmModRegister) + info.HasMemoryOperand = true; if (mod == ModRmModIndirect && rm == ModRmRmRipRelative) { info.HasRipRelativeDisp = true; info.RipRelativeDispOffset = modrmPos + 1; @@ -404,9 +409,17 @@ DecodedInstructionInfo X64InstructionDecoder::DecodeInstruction( const bool xop = op == XopPrefix && pos < info.Length && (data[pos] & Vex3MapMask) >= XopMapMin; if (op == OneByteVex2 || op == OneByteVex3 || op == EvexPrefix || xop) { - pos += (op == OneByteVex2 ? 1 : (op == OneByteVex3 || xop ? 2 : EvexPrefixLength - 1)) + 1; - if (pos < info.Length) + const bool vex = op == OneByteVex2 || op == OneByteVex3; + const std::size_t vectorOpcodeOffset = pos + (op == OneByteVex2 ? 1 : (op == OneByteVex3 || xop ? 2 : EvexPrefixLength - 1)); + const std::uint8_t vexMap = op == OneByteVex2 ? 1 : (data[pos] & (op == EvexPrefix ? EvexMapMask : Vex3MapMask)); + const std::uint8_t vectorOpcode = vectorOpcodeOffset < info.Length ? data[vectorOpcodeOffset] : 0; + pos = vectorOpcodeOffset + 1; + if (pos < info.Length) { readModRm(pos, false); + if (vex && vexMap == 1 && vectorOpcode == TwoByteMaskmov && + ((info.ModRmByte >> ModRmModShift) & ModRmModMask) == ModRmModRegister) + info.HasMemoryOperand = true; + } info.FlowKind = ControlFlowKind::Sequential; return info; } @@ -480,6 +493,8 @@ DecodedInstructionInfo X64InstructionDecoder::DecodeInstruction( info.ModRmRegField = reg; std::uint8_t mod = (modrm >> ModRmModShift) & ModRmModMask; std::uint8_t rm = modrm & ModRmRmMask; + if (mod != ModRmModRegister) + info.HasMemoryOperand = true; if (reg == 2) { if (mod == ModRmModIndirect && rm == ModRmRmRipRelative) { info.FlowKind = ControlFlowKind::Call; @@ -592,6 +607,21 @@ DecodedInstructionInfo X64InstructionDecoder::DecodeInstruction( if (hasModRm && pos < info.Length) readModRm(pos, twoByteOpcode && op >= TwoByteMovCrDrMin && op <= TwoByteMovCrDrMax); + if (info.HasModRm && twoByteOpcode) { + const std::uint8_t mod = (info.ModRmByte >> ModRmModShift) & ModRmModMask; + const std::uint8_t reg = (info.ModRmByte >> ModRmRegShift) & ModRmRegMask; + if (op == TwoByteMaskmov && mod == ModRmModRegister) + info.HasMemoryOperand = true; + else if (op == TwoByteGrp7 && (info.ModRmByte == 0xC8 || info.ModRmByte == 0xFA || info.ModRmByte == 0xFC)) + info.HasMemoryOperand = true; + else if (op == TwoByteGrp15 && repPrefix && reg == 6 && mod == ModRmModRegister) + info.HasMemoryOperand = true; + } + + if (!twoByteOpcode && ((op >= 0x6c && op <= 0x6f) || (op >= 0xa0 && op <= 0xa7) || + (op >= 0xaa && op <= 0xaf) || op == 0xd7)) + info.HasMemoryOperand = true; + info.FlowKind = ControlFlowKind::Sequential; return info; } diff --git a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp index 89bc80f07d..08d527462d 100644 --- a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp +++ b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -349,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}); @@ -489,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); @@ -611,15 +621,39 @@ 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 converterInPlaceFollower() { + const auto converter = Codegen::MakeAmd64OnlyConverter(); + const auto convert = [&](const Bytes& following) { + Bytes file(0x300, 0xCC); + Bytes text = {0x66, 0x0F, 0x79, 0xCA}; + text.insert(text.end(), following.begin(), following.end()); + text.push_back(0xC3); + std::copy(text.begin(), text.end(), file.begin() + 0x200); + return converter->Convert(file, {segmentHeader(text.size())}); + }; + const auto tail = [](const Codegen::TrampolineSite& site, const std::size_t length) { + return Bytes(site.Body.begin() + static_cast(site.ReturnBranchOffset - length), site.Body.begin() + static_cast(site.ReturnBranchOffset)); + }; + const auto store = convert({0xF3, 0x0F, 0x2B, 0x07}); + require(store.Trampolines.size() == 1 && store.Trampolines[0].Length == 8 && tail(store.Trampolines[0], 4) == Bytes{0xF3, 0x0F, 0x11, 0x07}, "MOVNTSS after a short EXTRQ was not moved into its stub as MOVSS"); + const auto ripStore = convert({0xF2, 0x0F, 0x2B, 0x05, 0x40, 0x00, 0x00, 0x00}); + require(ripStore.Trampolines.size() == 1 && ripStore.Trampolines[0].Length == 12, "RIP-relative MOVNTSD after a short EXTRQ was not moved into its stub"); + const auto& ripSite = ripStore.Trampolines[0]; + require(tail(ripSite, 8) == Bytes{0xF2, 0x0F, 0x11, 0x05, 0x00, 0x00, 0x00, 0x00} && ripSite.Relocations.size() == 1 && ripSite.Relocations[0].SiteTarget == 12 + 0x40 && ripSite.Relocations[0].InstructionEnd == ripSite.ReturnBranchOffset, "Moved MOVNTSD has the wrong bytes or relocation"); + const auto monitor = convert({0x0F, 0x01, 0xFA}); + require(monitor.Trampolines.size() == 1 && monitor.Trampolines[0].Length == 7 && tail(monitor.Trampolines[0], 3) == Bytes{0x0F, 0x1F, 0x00}, "MONITORX after a short EXTRQ was not moved into its stub as a NOP"); } void converterClzero() { @@ -885,6 +919,14 @@ void converterWideStoreWindows() { expected.push_back(0xC3); require(returnIsland.Trampolines.size() == 2 && returnIsland.Trampolines[0].Offset == 0x20B && body(returnIsland.Trampolines[0]) == expected, "Instructions moved for an island that end in a return jump back after it"); + const auto intoStore = convert(withStore({0x73, 0x01}, {0xC3})); + expected = halves; + expected.push_back(0xC3); + require(intoStore.Trampolines.size() == 1 && intoStore.Trampolines[0].Offset == 0x202 && intoStore.Trampolines[0].Length == 5 && body(intoStore.Trampolines[0]) == expected, "A branch into the middle of a 256-bit store, where no scanned instruction starts, kept the store"); + + const auto intoFollower = convert(withStore({0x73, 0x06}, {0x48, 0x83, 0xC1, 0x01, 0xC3})); + require(intoFollower.Trampolines.size() == 1 && intoFollower.Trampolines[0].Offset == 0x202 && intoFollower.Trampolines[0].Length == 8, "A branch into the middle of an instruction moved with a 256-bit store kept the store"); + const auto afterBranch = convert(withStore({0x75, 0x10}, {0xFF, 0xD0})); expected = {0x0F, 0x85, 0, 0, 0, 0}; expected.insert(expected.end(), halves.begin(), halves.end()); @@ -904,6 +946,32 @@ void converterWideStoreWindows() { requireFailure([&] { (void)convert(withStore(far, tail)); }, "A 256-bit store whose movable neighbours are all beyond a short jump was kept"); } +void converterClaimedFollower() { + const auto converter = Codegen::MakeAmd64OnlyConverter(Codegen::Amd64OnlyTarget::Rosetta); + for (const auto& [shortInstruction, name] : {std::pair{Bytes{0x0F, 0x53, 0xCA}, "RCPPS"}, std::pair{Bytes{0xF3, 0x0F, 0xC7, 0xF8}, "RDPID"}}) { + Bytes file(0x300, 0xCC); + Bytes text = {0x74, 0x06, 0xEB, 0x00, 0xC5, 0xFC, 0x11, 0x07}; + text.insert(text.end(), shortInstruction.begin(), shortInstruction.end()); + for (int index = 0; index < 4; ++index) + text.insert(text.end(), {0x48, 0x01, 0xC8}); + text.insert(text.end(), 8, 0x90); + text.push_back(0xC3); + std::copy(text.begin(), text.end(), file.begin() + 0x200); + const auto result = converter->Convert(file, {segmentHeader(text.size())}); + for (std::size_t left = 0; left < result.Trampolines.size(); ++left) { + for (std::size_t right = left + 1; right < result.Trampolines.size(); ++right) { + const auto& a = result.Trampolines[left]; + const auto& b = result.Trampolines[right]; + require(a.Offset + a.Length <= b.Offset || b.Offset + b.Length <= a.Offset, "A short instruction's site overlaps instructions another stub already moved"); + } + } + bool kept = false; + for (const auto& report : result.Reports) + kept = kept || (report.InstructionName == name && report.Lowering == Codegen::Amd64OnlyLowering::Kept); + require(result.KeptCount == 1 && kept, "An optional short instruction followed by moved instructions was not kept"); + } +} + void converterJumpTableTargets() { const auto converter = Codegen::MakeAmd64OnlyConverter(Codegen::Amd64OnlyTarget::Rosetta); const Bytes leaTable = {0x48, 0x8D, 0x0D, 0xF9, 0x1F, 0x00, 0x00}; @@ -1054,10 +1122,15 @@ void converterRosettaScalarCompare() { require(site.Offset == 0x20C && site.Length == 5 && site.ReturnBranchOffset == 5 && Bytes(site.Body.begin(), site.Body.begin() + 5) == Bytes(text.begin() + 12, text.begin() + 17) && Bytes(site.Body.begin() + 5, site.Body.begin() + 10) == Bytes{0xE9, 0, 0, 0, 0} && site.Relocations.empty(), "The compare stub does not run the compare unchanged and return"); require(result.Bytes == file, "Moving the compare changed the file image"); auto branchInside = file; - const Bytes jumped = {0xEB, 0x02, 0xC5, 0xEA, 0xC2, 0xCA, 0x03, 0xC3}; + const Bytes jumped = {0xEB, 0x01, 0xF3, 0x0F, 0xC2, 0xCA, 0x03, 0xC3}; std::copy(jumped.begin(), jumped.end(), branchInside.begin() + 0x200); const auto kept = converter->Convert(branchInside, {segmentHeader(jumped.size())}); - require(kept.Trampolines.empty() && kept.KeptCount == 1 && kept.Reports.size() == 1 && kept.Reports[0].Lowering == Codegen::Amd64OnlyLowering::Kept && kept.Bytes == branchInside, "A branch into the compare did not keep it native"); + require(kept.Trampolines.empty() && kept.KeptCount == 1 && kept.Reports.size() == 1 && kept.Reports[0].Lowering == Codegen::Amd64OnlyLowering::Kept && kept.Bytes == branchInside, "A branch past the prefix of the compare did not keep it native"); + auto branchIntoVex = file; + const Bytes vexJumped = {0xEB, 0x02, 0xC5, 0xEA, 0xC2, 0xCA, 0x03, 0xC3}; + std::copy(vexJumped.begin(), vexJumped.end(), branchIntoVex.begin() + 0x200); + const auto moved = converter->Convert(branchIntoVex, {segmentHeader(vexJumped.size())}); + require(moved.Trampolines.size() == 1 && moved.Trampolines[0].Offset == 0x202 && moved.KeptCount == 0, "A branch past the VEX prefix of the compare, where no instruction starts, kept the compare native"); auto ripSource = file; const Bytes ripText = {0xC5, 0xEA, 0xC2, 0x0D, 0x00, 0x01, 0x00, 0x00, 0x03, 0xC3}; std::copy(ripText.begin(), ripText.end(), ripSource.begin() + 0x200); @@ -1201,10 +1274,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) { @@ -1255,9 +1328,10 @@ Elfpatcher::Linux::LinuxElfPatcher linuxPatcher() { } std::uint64_t executableFileOffset(const Bytes& output, const std::uint64_t address) { + const auto phOff = read(output, 32); const auto phNum = read(output, 56); for (std::uint16_t index = 0; index < phNum; ++index) { - const auto header = 64 + index * 56; + const auto header = phOff + index * 56; if (read(output, header) != 1) continue; const auto vaddr = read(output, header + 16); const auto memSize = read(output, header + 40); @@ -1305,6 +1379,58 @@ void linuxRipRelativePlacement() { require(operand == 0x2040, "Relocated RIP-relative operand does not reach the original address"); } +std::vector> stackHeaders(const Bytes& output) { + const auto phOff = read(output, 32); + const auto phNum = read(output, 56); + std::vector> found; + for (std::uint16_t index = 0; index < phNum; ++index) { + const auto header = static_cast(phOff) + index * 56; + if (read(output, header) != Elfpatcher::PT_GNU_STACK) continue; + found.push_back({read(output, header), read(output, header + 4), + read(output, header + 8), read(output, header + 16), + read(output, header + 24), read(output, header + 32), + read(output, header + 40), read(output, header + 48)}); + } + return found; +} + +void linuxStackSynthesis() { + const auto patchOutput = [](std::uint16_t slots, std::vector headers) { + auto elf = elfFixture({0xC3}); + write(elf, 56, slots); + auto patcher = linuxPatcher(); + return patcher.Patch(std::move(elf), std::move(headers), {}, 0, "$ORIGIN/libs", true, false, {}); + }; + const Domain::ProgramHeader inputStack{Elfpatcher::PT_GNU_STACK, 6, 0, 0, 0, 0, 0x20000, 16}; + const std::vector> synthetic = {{Elfpatcher::PT_GNU_STACK, 6, 0, 0, 0, 0, 0, 16}}; + const std::vector> preserved = {{Elfpatcher::PT_GNU_STACK, 6, 0, 0, 0, 0, 0x20000, 16}}; + + const auto exactFit = patchOutput(7, elfHeaders()); + require(read(exactFit, 32) >= 0x400, "a full input table must move to make room for the synthetic stack"); + require(read(exactFit, 56) == 8, "the output must use the reserved synthetic stack slot"); + require(stackHeaders(exactFit) == synthetic, "the synthetic stack must be PT_GNU_STACK with PF_R|PF_W and alignment 16"); + + const auto spareSlot = patchOutput(8, elfHeaders()); + require(read(spareSlot, 32) == 64, "the required count must not move a table that has a free slot"); + require(read(spareSlot, 56) == 8, "the free slot must be filled by the synthetic stack"); + require(stackHeaders(spareSlot) == synthetic, "the synthetic stack must be PT_GNU_STACK with PF_R|PF_W and alignment 16"); + + const auto roomyTable = patchOutput(9, elfHeaders()); + require(read(roomyTable, 32) == 64, "the required count must not move a table with spare slots"); + require(read(roomyTable, 56) == 8, "the output count must match the required count"); + require(stackHeaders(roomyTable) == synthetic, "the synthetic stack must be PT_GNU_STACK with PF_R|PF_W and alignment 16"); + + const auto keptStackExact = patchOutput(8, {elfHeaders()[0], elfHeaders()[1], inputStack}); + require(read(keptStackExact, 32) == 64, "an input stack must not over-reserve a header slot"); + require(read(keptStackExact, 56) == 8, "an input stack must not grow the output count"); + require(stackHeaders(keptStackExact) == preserved, "a kept PT_GNU_STACK must stay unchanged"); + + const auto keptStackShort = patchOutput(7, {elfHeaders()[0], elfHeaders()[1], inputStack}); + require(read(keptStackShort, 32) >= 0x400, "a full input table must move when the kept stack does not fit"); + require(read(keptStackShort, 56) == 8, "the moved table must carry the kept stack"); + require(stackHeaders(keptStackShort) == preserved, "a kept PT_GNU_STACK must stay unchanged"); +} + } #if (defined(__linux__) || defined(__APPLE__)) && defined(__x86_64__) @@ -1740,6 +1866,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() {} @@ -1748,6 +1960,8 @@ void sha256Execution() {} void clzeroExecution() {} void immediateFormExecution() {} void ripRelativeExecution() {} +void rdpruExecution() {} +void mcommitExecution() {} #endif #if defined(__x86_64__) && (defined(__linux__) || defined(__APPLE__)) @@ -1792,6 +2006,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(); @@ -1813,11 +2039,14 @@ int main() { clzeroExecution(); reciprocalExecution(); ripRelativeExecution(); + rdpruExecution(); + mcommitExecution(); converterSegment(); converterRipRelativeFollower(); converterSha256(); converterSha1(); converterMonitorWait(); + converterInPlaceFollower(); converterClzero(); rosettaSubstitutions(); converterRosetta(); @@ -1828,6 +2057,7 @@ int main() { rdtscpExecution(); wideStoreSplit(); converterWideStoreWindows(); + converterClaimedFollower(); converterJumpTableTargets(); converterReciprocal(); converterStrayRex(); @@ -1836,7 +2066,9 @@ int main() { converterFailureOffsets(); linuxPlacement(); 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/codegen/tests/DecoderLengthTests.cpp b/core/relinker/codegen/tests/DecoderLengthTests.cpp index 63bca4bfe0..3ad1299b0d 100644 --- a/core/relinker/codegen/tests/DecoderLengthTests.cpp +++ b/core/relinker/codegen/tests/DecoderLengthTests.cpp @@ -117,6 +117,32 @@ int main() { const auto threeDNowInfo = decoder.DecodeInstruction(threeDNow.data(), threeDNow.size()); require(threeDNowInfo.HasModRm && threeDNowInfo.ModRmByte == 0x0d, "3DNow! ModRM was not reported"); require(threeDNowInfo.HasRipRelativeDisp && threeDNowInfo.RipRelativeDispOffset == 3, "3DNow! RIP-relative displacement was not reported"); + for (const auto& testCase : std::vector>{ + {"6590", false}, + {"654889c8", false}, + {"65488b0c2578030000", true}, + {"65a4", true}, + {"650fa2", false}, + {"650ff7c1", true}, + {"65660ff7c1", true}, + {"65c5f9f7c1", true}, + {"65c4e179f7c1", true}, + {"650f01c8", true}, + {"650f01fa", true}, + {"650f01fc", true}, + {"650f01c9", false}, + {"650f01fd", false}, + {"65f30faef0", true}, + {"f20faef0", false}, + {"660faef0", false}, + {"f30faee8", false}, + {"ff10", true}, + {"ffe0", false}, + }) { + const auto bytes = parse(testCase.first); + require(decoder.DecodeInstruction(bytes.data(), bytes.size()).HasMemoryOperand == testCase.second, + "Wrong memory operand presence for " + std::string(testCase.first)); + } std::cout << "Decoder length 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/include/elfpatcher/general/ElfConstants.hpp b/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp index db9c782901..9da009bc08 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp @@ -1,6 +1,7 @@ #ifndef ELFPATCHER_DOMAIN_ELFCONSTANTS_HPP #define ELFPATCHER_DOMAIN_ELFCONSTANTS_HPP +#include #include namespace Elfpatcher { diff --git a/core/relinker/elfpatcher/include/elfpatcher/general/GuestModuleWriter.hpp b/core/relinker/elfpatcher/include/elfpatcher/general/GuestModuleWriter.hpp index a59418beea..5ca491e9bf 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/general/GuestModuleWriter.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/general/GuestModuleWriter.hpp @@ -2,14 +2,16 @@ #define ELFPATCHER_GUESTMODULEWRITER_HPP #include +#include +#include namespace Elfpatcher { class GuestModuleWriter { public: std::vector WriteLinux(const Relinker::GuestImage& image, const std::vector& dependencies, const std::string& runPath) const; - std::vector WriteWindows(const Relinker::GuestImage& image, Domain::GuestRuntime& runtime) const; - std::vector WriteMacOs(const Relinker::GuestImage& image, const std::vector& dependencies, const std::string& runPath) const; + std::vector WriteWindows(const Relinker::GuestImage& image, Domain::GuestRuntime& runtime, const std::function& message = {}) const; + std::vector WriteMacOs(const Relinker::GuestImage& image, const std::vector& dependencies, const std::string& runPath, const std::function& message = {}) const; }; } diff --git a/core/relinker/elfpatcher/include/elfpatcher/general/ProgramHeaderLayoutBuilder.hpp b/core/relinker/elfpatcher/include/elfpatcher/general/ProgramHeaderLayoutBuilder.hpp index 4e495a72a6..4d89ad7698 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/general/ProgramHeaderLayoutBuilder.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/general/ProgramHeaderLayoutBuilder.hpp @@ -36,6 +36,7 @@ namespace Elfpatcher { [[nodiscard]] Domain::ProgramHeader _makeInterpHeader(std::uint64_t offset, std::uint64_t vaddr, std::uint64_t size) const; [[nodiscard]] std::uint32_t _fixLoadFlags(std::uint32_t originalFlags) const; [[nodiscard]] std::vector _keptHeaders(const std::vector& originalHeaders) const; + [[nodiscard]] std::vector _outputHeaders(const std::vector& originalHeaders) const; void _sortLoadHeaders(std::vector& headers) const; void _writeProgramHeader(std::vector& buf, std::size_t offset, const Domain::ProgramHeader& ph) const; }; diff --git a/core/relinker/elfpatcher/include/elfpatcher/linux/LinuxElfPatcher.hpp b/core/relinker/elfpatcher/include/elfpatcher/linux/LinuxElfPatcher.hpp index 17f12e8fc6..c24b3114f0 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/linux/LinuxElfPatcher.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/linux/LinuxElfPatcher.hpp @@ -21,7 +21,8 @@ public: std::shared_ptr programHeaderLayoutBuilder, std::shared_ptr sectionHeaderTableBuilder, std::shared_ptr byteWriter, - std::optional fixedBase = std::nullopt + std::optional fixedBase = std::nullopt, + bool traceImports = false ); std::vector Patch( @@ -41,6 +42,7 @@ private: std::shared_ptr _sectionHeaderTableBuilder; std::shared_ptr _byteWriter; std::optional _fixedBase; + bool _traceImports; void _appendDynEntry(std::vector& buf, std::int64_t tag, std::uint64_t val) const; void _appendTrampoline(std::vector& buf, const Codegen::TrampolineSite& site, std::uint64_t extraBlockOffset, const std::function& vaddrOfExtraBlockOffset) const; diff --git a/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsImage.hpp b/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsImage.hpp index 4f7ec0fee8..99bc214e54 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsImage.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsImage.hpp @@ -5,11 +5,16 @@ #include #include #include +#include #include +#include #include namespace Elfpatcher::MacOs { +inline constexpr std::uint64_t ExportKindAbsolute = 2; +inline constexpr std::uint64_t ExportWeakDefinition = 4; + struct MacOsBind { std::string Symbol; std::uint32_t Rva; @@ -19,7 +24,8 @@ struct MacOsBind { struct MacOsExport { std::string Symbol; - std::uint32_t Rva; + std::uint64_t Value; + std::uint64_t Flags = 0; }; struct MacOsTlsImport { @@ -32,6 +38,7 @@ struct MacOsTlsImport { struct MacOsTlsExport { std::string Symbol; std::uint64_t Offset; + std::uint64_t Flags = 0; }; struct MacOsImageInput { @@ -54,6 +61,7 @@ struct MacOsImageInput { std::vector TlsImports; std::vector TlsExports; const std::vector* Trampolines = nullptr; + std::function Message; }; std::uint64_t GuestTlsBlockSize(const Domain::ProgramHeader& tls); diff --git a/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsMachOPatcher.hpp b/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsMachOPatcher.hpp index 397c96e826..2f2ffbc485 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsMachOPatcher.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsMachOPatcher.hpp @@ -3,13 +3,15 @@ #include #include +#include #include +#include namespace Elfpatcher::MacOs { class MacOsMachOPatcher final : public IElfPatcher { public: - explicit MacOsMachOPatcher(std::optional fixedBase = std::nullopt); + explicit MacOsMachOPatcher(std::optional fixedBase = std::nullopt, std::function message = {}); std::vector Patch( std::vector sourceElf, @@ -24,6 +26,7 @@ public: private: std::optional fixedBase; + std::function message; }; } diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsElfPatcher.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsElfPatcher.hpp index 6fe65d2fc7..07db6f888a 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsElfPatcher.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsElfPatcher.hpp @@ -6,12 +6,13 @@ #include #include #include +#include namespace Elfpatcher::Windows { class WindowsPePatcher : public IElfPatcher { public: - explicit WindowsPePatcher(bool windowsGui = false, std::optional> icon = std::nullopt, std::filesystem::path iconName = "icon0.png", std::function progress = {}, std::optional fixedBase = std::nullopt, bool traceImports = false); + explicit WindowsPePatcher(bool windowsGui = false, std::optional> icon = std::nullopt, std::filesystem::path iconName = "icon0.png", std::function progress = {}, std::optional fixedBase = std::nullopt, bool traceImports = false, std::function message = {}); const std::string& GetDiagnostics() const; std::vector Patch(std::vector sourceElf, const std::vector& originalHeaders, const Domain::SysVDynamicSection& dynamicSection, std::uint64_t originalPltGotVaddr, const std::string& runPath, bool lazyBinding, bool dependencyDiagnostics, const std::vector& trampolines) override; @@ -24,6 +25,7 @@ private: std::filesystem::path iconName; std::optional fixedBase; bool traceImports; + std::function message; }; } diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsLoadImage.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsLoadImage.hpp index 3f2fe28f13..fa2ef59297 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsLoadImage.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsLoadImage.hpp @@ -24,7 +24,7 @@ private: std::vector Bytes; }; - Block& findBlock(std::uint64_t offset); + Block& backBlock(std::uint64_t offset, std::uint64_t length); std::vector segments; std::vector blocks; diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeFormat.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeFormat.hpp index fec5214e01..790dfdf3ae 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeFormat.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeFormat.hpp @@ -27,6 +27,16 @@ struct PeSection { std::uint64_t MemorySize() const { return std::max(Data.size(), VirtualSize); } + + bool Writable(const std::uint32_t rva, const std::uint64_t size) { + if (rva < Rva || rva - Rva > MemorySize() || size > MemorySize() - (rva - Rva)) + return false; + if (rva - Rva + size > Data.size()) { + VirtualSize = static_cast(MemorySize()); + Data.resize(static_cast(rva - Rva + size)); + } + return true; + } }; struct PeDirectory { diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsTlsBuilder.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsTlsBuilder.hpp index 207ef639bf..39b4a54f87 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsTlsBuilder.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsTlsBuilder.hpp @@ -3,12 +3,13 @@ #include #include +#include namespace Elfpatcher::Windows { class WindowsTlsBuilder { public: - PeDirectory Build(const std::vector& source, const std::vector& headers, const WindowsLoadImage& image, std::vector& sections, std::vector& relocations, std::uint32_t& nextRva, std::uint32_t* tlsIndexRva = nullptr, const std::function& progress = {}) const; + PeDirectory Build(const std::vector& source, const std::vector& headers, const WindowsLoadImage& image, std::vector& sections, std::vector& relocations, std::uint32_t& nextRva, std::uint32_t* tlsIndexRva = nullptr, const std::function& progress = {}, const std::function& message = {}) const; }; } diff --git a/core/relinker/elfpatcher/src/general/ProgramHeaderLayoutBuilder.cpp b/core/relinker/elfpatcher/src/general/ProgramHeaderLayoutBuilder.cpp index af09d65c99..fc23e28dea 100644 --- a/core/relinker/elfpatcher/src/general/ProgramHeaderLayoutBuilder.cpp +++ b/core/relinker/elfpatcher/src/general/ProgramHeaderLayoutBuilder.cpp @@ -148,8 +148,15 @@ std::vector ProgramHeaderLayoutBuilder::_keptHeaders(cons return kept; } +std::vector ProgramHeaderLayoutBuilder::_outputHeaders(const std::vector& originalHeaders) const { + auto kept = _keptHeaders(originalHeaders); + if (std::none_of(kept.begin(), kept.end(), [](const Domain::ProgramHeader& ph) { return ph.Type == PT_GNU_STACK; })) + kept.push_back(Domain::ProgramHeader{PT_GNU_STACK, PF_R | PF_W, 0, 0, 0, 0, 0, 16}); + return kept; +} + std::size_t ProgramHeaderLayoutBuilder::RequiredProgramHeaderCount(const std::vector& originalHeaders) const { - return _keptHeaders(originalHeaders).size() + kSyntheticProgramHeaderCount; + return _outputHeaders(originalHeaders).size() + kSyntheticProgramHeaderCount; } std::uint64_t ProgramHeaderLayoutBuilder::GuestLoadPageEnd( @@ -163,6 +170,8 @@ std::uint64_t ProgramHeaderLayoutBuilder::GuestLoadPageEnd( if (ph.Type != PT_LOAD) continue; foundLoad = true; + if (ph.FileSize > ph.MemorySize) + throw Domain::RelinkerException("PT_LOAD file size exceeds memory size", ph.Offset); if (ph.MemorySize > std::numeric_limits::max() - ph.MappedAddress) throw Domain::RelinkerException("Load segment virtual range overflowed"); const std::uint64_t vaddrEnd = ph.MappedAddress + ph.MemorySize; @@ -180,7 +189,7 @@ std::uint16_t ProgramHeaderLayoutBuilder::WriteLayout( std::vector& buf, const ProgramHeaderLayoutRequest& request ) const { - auto kept = _keptHeaders(request.OriginalHeaders); + auto kept = _outputHeaders(request.OriginalHeaders); const auto frameCount = std::count_if(kept.begin(), kept.end(), [](const Domain::ProgramHeader& ph) { return ph.Type == PT_GNU_EH_FRAME; }); const bool keepFrames = kept.size() + kSyntheticProgramHeaderCount <= request.PhNum; @@ -206,8 +215,12 @@ std::uint16_t ProgramHeaderLayoutBuilder::WriteLayout( if (tableEnd > buf.size()) throw Domain::RelinkerException("Program header table exceeds the output file"); const bool tableInExtraBlock = request.PhOff >= request.ExtraBlockOffset; - if (tableInExtraBlock && tableEnd - request.ExtraBlockOffset > request.ExtraBlockSize) - throw Domain::RelinkerException("Program header table does not fit within the extra block"); + if (tableInExtraBlock) { + if (tableEnd - request.ExtraBlockOffset > request.ExtraBlockSize) + throw Domain::RelinkerException("Program header table does not fit within the extra block"); + } else if (tableEnd > request.ExtraBlockOffset) { + throw Domain::RelinkerException("Program header table overlaps the extra block"); + } const std::uint64_t headerBlockSize = tableInExtraBlock ? kElfHeaderSize : tableEnd; if (request.ExtraBlockOffset > buf.size() || request.ExtraBlockSize > buf.size() - request.ExtraBlockOffset) throw Domain::RelinkerException("Extra block exceeds the output file"); diff --git a/core/relinker/elfpatcher/src/general/SectionHeaderTableBuilder.cpp b/core/relinker/elfpatcher/src/general/SectionHeaderTableBuilder.cpp index a7c4cc1f0f..e07068514f 100644 --- a/core/relinker/elfpatcher/src/general/SectionHeaderTableBuilder.cpp +++ b/core/relinker/elfpatcher/src/general/SectionHeaderTableBuilder.cpp @@ -1,8 +1,10 @@ #include #include #include -#include +#include #include +#include +#include namespace Elfpatcher { @@ -41,14 +43,39 @@ void SectionHeaderTableBuilder::WriteTable( std::vector& buf, const SectionHeaderTableRequest& request ) const { + if (buf.size() < kElfHeaderSize) + throw std::runtime_error("Output program header table is out of bounds"); const auto phOffset = Io::ReadU64(buf, kEhdrPhOffOffset); const auto phCount = Io::ReadU16(buf, kEhdrPhNumOffset); const auto phStride = Io::ReadU16(buf, kEhdrPhEntSizeOffset); - if (phStride < 56 || phOffset > buf.size() || phCount > (buf.size() - phOffset) / phStride) + if (phStride < 56 || (phCount > 0 && phOffset < kElfHeaderSize) || phOffset > buf.size() || phCount > (buf.size() - phOffset) / phStride) throw std::runtime_error("Output program header table is out of bounds"); + const auto phTableEnd = phOffset + static_cast(phCount) * phStride; + const std::array, 4> sections{{ + {request.DynStrOffset, request.DynStrSize}, + {request.DynSymOffset, request.DynSymSize}, + {request.DynamicSegmentOffset, request.DynamicSegmentSize}, + {request.StubOffset, request.StubSize}, + }}; + for (std::size_t i = 0; i < sections.size(); ++i) { + if (sections[i].second == 0) continue; + for (std::size_t j = i + 1; j < sections.size(); ++j) { + if (sections[j].second == 0) continue; + const auto startA = sections[i].first; + const auto endA = startA + sections[i].second; + const auto startB = sections[j].first; + const auto endB = startB + sections[j].second; + if (startA < endB && startB < endA) + throw std::runtime_error("Allocated output sections overlap"); + } + } const auto mappedAddress = [&](std::uint64_t offset, std::uint64_t sectionSize) { if (offset > buf.size() || sectionSize > buf.size() - offset) throw std::runtime_error("Allocated output section exceeds file"); + if (offset < kElfHeaderSize && sectionSize > 0) + throw std::runtime_error("Allocated output section overlaps the ELF header"); + if (sectionSize > 0 && phCount > 0 && offset < phTableEnd && phOffset < offset + sectionSize) + throw std::runtime_error("Allocated output section overlaps the program header table"); for (std::uint16_t index = 0; index < phCount; ++index) { const auto header = phOffset + index * phStride; if (Io::ReadU32(buf, header) != PT_LOAD) continue; diff --git a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp index 88e0279e25..df116a307c 100644 --- a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp +++ b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp @@ -7,9 +7,11 @@ #include #include #include +#include #include #include #include +#include #include namespace Elfpatcher::Linux { @@ -143,6 +145,75 @@ void SeparateSharedLoadPages(std::vector& buf, std::vector& strings, const std::uint32_t offset) { + if (offset >= strings.size()) + throw Domain::RelinkerException("Import symbol name lies outside the dynamic string table"); + std::string value; + for (std::size_t index = offset; index < strings.size() && strings[index] != 0; ++index) + value.push_back(static_cast(strings[index])); + return value; +} + +std::vector TracedImports(const Domain::SysVDynamicSection& dynSection) { + std::vector imports; + const auto collect = [&](const std::vector& table) { + RequireWholeEntries(table, kRelaEntrySize, "Relocation table"); + for (std::size_t entry = 0; entry < table.size(); entry += kRelaEntrySize) { + const auto info = Io::ReadU64(table, entry + 8); + const auto type = static_cast(info); + const auto symbol = info >> 32; + const auto target = Io::ReadU64(table, entry); + if ((type != R_X86_64_GLOB_DAT && type != R_X86_64_JUMP_SLOT) || symbol == 0 || Io::ReadU64(table, entry + 16) != 0) + continue; + if (type == R_X86_64_GLOB_DAT && !dynSection.FunctionImports.contains(target)) + continue; + if (symbol >= dynSection.DynSymData.size() / kSymEntrySize) + throw Domain::RelinkerException("Invalid import symbol index", target); + const auto symbolOffset = static_cast(symbol * kSymEntrySize); + if (Io::ReadU16(dynSection.DynSymData, symbolOffset + 6) != SHN_UNDEF || dynSection.DynSymData[symbolOffset + 4] >> 4 == STB_WEAK) + continue; + auto name = ReadDynamicString(dynSection.DynStrData, Io::ReadU32(dynSection.DynSymData, symbolOffset)); + if (name.empty()) + continue; + const auto library = dynSection.ImportModules.find(target); + imports.push_back({target, std::move(name), library == dynSection.ImportModules.end() ? std::string{} : library->second}); + } + }; + collect(dynSection.RelaData); + collect(dynSection.RelaPltData); + return imports; +} + +std::vector TracePrologue(const std::uint64_t vaddr, const std::uint64_t table, const std::uint64_t tableEnd, const std::uint64_t function) { + std::vector code = {0x52, 0x53, 0x41, 0x54, 0x48, 0x83, 0xec, 0x08}; + const auto rip = [&](std::initializer_list opcode, const std::uint64_t target) { + code.insert(code.end(), opcode); + const auto displacement = static_cast(target) - static_cast(vaddr + code.size() + 4); + if (displacement < std::numeric_limits::min() || displacement > std::numeric_limits::max()) + throw Domain::RelinkerException("Import trace table exceeds rel32 range"); + Io::AppendU32(code, static_cast(displacement)); + }; + rip({0x48, 0x8d, 0x1d}, table); + rip({0x4c, 0x8d, 0x25}, tableEnd); + const auto loop = code.size(); + code.insert(code.end(), {0x4c, 0x39, 0xe3, 0x73, 0x00}); + const auto exit = code.size(); + code.insert(code.end(), {0x48, 0x8b, 0x03, 0x48, 0x8b, 0x3c, 0x03, 0x48, 0x8b, 0x73, 0x08, 0x48, 0x01, 0xde, 0x48, 0x8b, 0x53, 0x10, 0x48, 0x01, 0xda}); + rip({0xff, 0x15}, function); + code.insert(code.end(), {0x48, 0x8b, 0x3b, 0x48, 0x01, 0xdf, 0x48, 0x81, 0xe7, 0x00, 0xf0, 0xff, 0xff, 0xbe, 0x00, 0x10, 0x00, 0x00, 0xba, 0x07, 0x00, 0x00, 0x00, 0x50, 0xb8, 0x0a, 0x00, 0x00, 0x00, 0x0f, 0x05, 0x58}); + code.insert(code.end(), {0x48, 0x8b, 0x0b, 0x48, 0x89, 0x04, 0x0b, 0x48, 0x83, 0xc3, 0x18, 0xeb}); + code.push_back(static_cast(static_cast(static_cast(loop) - static_cast(code.size() + 1)))); + code[exit - 1] = static_cast(code.size() - exit); + code.insert(code.end(), {0x48, 0x83, 0xc4, 0x08, 0x41, 0x5c, 0x5b, 0x5a}); + return code; +} + void RebaseProgramHeaders(std::vector& buf, const std::uint64_t tableOffset, const std::uint16_t entrySize, const std::uint16_t count, const std::uint64_t base) { for (std::uint16_t index = 0; index < count; ++index) { const auto header = static_cast(tableOffset + static_cast(index) * entrySize); @@ -170,13 +241,15 @@ LinuxElfPatcher::LinuxElfPatcher( std::shared_ptr programHeaderLayoutBuilder, std::shared_ptr sectionHeaderTableBuilder, std::shared_ptr byteWriter, - std::optional fixedBase + std::optional fixedBase, + bool traceImports ) : _entryStubBuilder(std::move(entryStubBuilder)) , _programHeaderLayoutBuilder(std::move(programHeaderLayoutBuilder)) , _sectionHeaderTableBuilder(std::move(sectionHeaderTableBuilder)) , _byteWriter(std::move(byteWriter)) , _fixedBase(fixedBase) + , _traceImports(traceImports) { } @@ -235,8 +308,13 @@ std::vector LinuxElfPatcher::Patch( throw Domain::RelinkerException("Linux target does not support --windows-diagnostics"); if (_fixedBase && lazyBinding) throw Domain::RelinkerException("A fixed Linux base requires eager binding; --lazy-binding is incompatible"); + if (_traceImports && lazyBinding) + throw Domain::RelinkerException("--trace-imports conflicts with --lazy-binding"); + if (_traceImports && std::find(dynSection.NeededLibraries.begin(), dynSection.NeededLibraries.end(), "libc.prx") == dynSection.NeededLibraries.end()) + throw Domain::RelinkerException("--trace-imports requires libc.prx among the libraries"); if (_fixedBase) RequireOnlyDependencies(dynSection.DynamicSegmentData); + const auto traced = _traceImports ? TracedImports(dynSection) : std::vector{}; const std::uint64_t base = _fixedBase.value_or(0); const auto address = [this, base](const std::uint64_t vaddr) { return _fixedBase ? Rebased(base, vaddr, "Address") : vaddr; @@ -273,17 +351,32 @@ std::vector LinuxElfPatcher::Patch( for (char c : runPath) buf.push_back(static_cast(c)); buf.push_back(0); + const std::uint64_t traceSymbolStrOff = static_cast(buf.size()) - dynStrOff; + if (_traceImports) { + for (char c : std::string_view("TraceImport_nid_no_patch")) + buf.push_back(static_cast(c)); + buf.push_back(0); + } const std::uint64_t dynStrSize = static_cast(buf.size()) - dynStrOff; alignBuf(buf, kDynStrAlignment); const auto dynSymOff = static_cast(buf.size()); - const auto symbols = table(dynSection.DynSymData, false); + auto symbols = table(dynSection.DynSymData, false); + const auto traceSymbol = symbols.size() / kSymEntrySize; + if (_traceImports) { + Io::AppendU32(symbols, static_cast(traceSymbolStrOff)); + symbols.push_back(static_cast((STB_GLOBAL << 4) | STT_FUNC)); + symbols.push_back(0); + Io::AppendU16(symbols, SHN_UNDEF); + Io::AppendU64(symbols, 0); + Io::AppendU64(symbols, 0); + } for (std::uint8_t b : symbols) buf.push_back(b); alignBuf(buf, kDynSymAlignment); const auto hashOff = static_cast(buf.size()); - const auto symbolCount = static_cast(dynSection.DynSymData.size() / kSymEntrySize); + const auto symbolCount = static_cast(symbols.size() / kSymEntrySize); Io::AppendU32(buf, 1); Io::AppendU32(buf, symbolCount); Io::AppendU32(buf, symbolCount > 1 ? 1 : 0); @@ -292,7 +385,13 @@ std::vector LinuxElfPatcher::Patch( alignBuf(buf, kDynSymAlignment); const auto relaOff = static_cast(buf.size()); - const auto relocations = table(dynSection.RelaData, true); + auto relocations = table(dynSection.RelaData, true); + const auto traceRelocation = relocations.size(); + if (_traceImports) { + Io::AppendU64(relocations, 0); + Io::AppendU64(relocations, (static_cast(traceSymbol) << 32) | R_X86_64_GLOB_DAT); + Io::AppendU64(relocations, 0); + } for (std::uint8_t b : relocations) buf.push_back(b); alignBuf(buf, kRelaAlignment); @@ -321,9 +420,9 @@ std::vector LinuxElfPatcher::Patch( _appendDynEntry(dynSegBuf, DT_SYMENT, kSymEntrySize); _appendDynEntry(dynSegBuf, DT_HASH, address(vaddrOfExtraBlockOffset(hashOff))); _appendDynEntry(dynSegBuf, DT_DEBUG, 0); - if (!dynSection.RelaData.empty()) { + if (!relocations.empty()) { _appendDynEntry(dynSegBuf, DT_RELA, address(vaddrOfExtraBlockOffset(relaOff))); - _appendDynEntry(dynSegBuf, DT_RELASZ, dynSection.RelaData.size()); + _appendDynEntry(dynSegBuf, DT_RELASZ, relocations.size()); _appendDynEntry(dynSegBuf, DT_RELAENT, kRelaEntrySize); } if (!dynSection.RelaPltData.empty()) { @@ -341,12 +440,52 @@ std::vector LinuxElfPatcher::Patch( for (std::uint8_t b : dynSegBuf) buf.push_back(b); + std::uint64_t traceFunctionVaddr = 0; + std::uint64_t traceTableVaddr = 0; + std::uint64_t traceTableEndVaddr = 0; + std::vector traceEntries; + if (_traceImports) { + alignBuf(buf, 8); + const auto functionOff = static_cast(buf.size()); + traceFunctionVaddr = vaddrOfExtraBlockOffset(functionOff); + Io::AppendU64(buf, 0); + Io::WriteU64(buf, static_cast(relaOff + traceRelocation), address(traceFunctionVaddr)); + traceTableVaddr = vaddrOfExtraBlockOffset(buf.size()); + for (std::size_t index = 0; index < traced.size(); ++index) { + traceEntries.push_back(buf.size()); + buf.resize(buf.size() + 24); + } + traceTableEndVaddr = vaddrOfExtraBlockOffset(buf.size()); + std::map strings; + const auto addString = [&](const std::string& value) { + if (const auto existing = strings.find(value); existing != strings.end()) return existing->second; + const auto vaddr = vaddrOfExtraBlockOffset(buf.size()); + for (char c : value) + buf.push_back(static_cast(c)); + buf.push_back(0); + strings.emplace(value, vaddr); + return vaddr; + }; + for (std::size_t index = 0; index < traced.size(); ++index) { + const auto entryVaddr = vaddrOfExtraBlockOffset(traceEntries[index]); + const auto relative = [entryVaddr](const std::uint64_t vaddr) { return vaddr - entryVaddr; }; + Io::WriteU64(buf, traceEntries[index], relative(traced[index].Slot)); + Io::WriteU64(buf, traceEntries[index] + 8, relative(addString(traced[index].Name))); + Io::WriteU64(buf, traceEntries[index] + 16, relative(addString(traced[index].Library))); + } + } + const std::uint64_t realEntryVaddr = *reinterpret_cast(buf.data() + kEhdrEntryOffset); const auto stubOff = static_cast(buf.size()); const auto stubVaddr = vaddrOfExtraBlockOffset(stubOff); - const auto stubBytes = _entryStubBuilder->BuildEntryStub(address(stubVaddr), address(realEntryVaddr)); - for (std::uint8_t b : stubBytes) + if (_traceImports) { + for (std::uint8_t b : TracePrologue(stubVaddr, traceTableVaddr, traceTableEndVaddr, traceFunctionVaddr)) + buf.push_back(b); + } + const auto entryStubVaddr = vaddrOfExtraBlockOffset(buf.size()); + for (std::uint8_t b : _entryStubBuilder->BuildEntryStub(address(entryStubVaddr), address(realEntryVaddr))) buf.push_back(b); + const auto stubSize = static_cast(buf.size()) - stubOff; _byteWriter->WriteU64(buf, kEhdrEntryOffset, address(stubVaddr)); for (const auto& site : trampolines) @@ -395,11 +534,11 @@ std::vector LinuxElfPatcher::Patch( sectionRequest.DynStrOffset = dynStrOff; sectionRequest.DynStrSize = dynStrSize; sectionRequest.DynSymOffset = dynSymOff; - sectionRequest.DynSymSize = dynSection.DynSymData.size(); + sectionRequest.DynSymSize = symbols.size(); sectionRequest.DynamicSegmentOffset = dynSegOff; sectionRequest.DynamicSegmentSize = dynSegBuf.size(); sectionRequest.StubOffset = stubOff; - sectionRequest.StubSize = stubBytes.size(); + sectionRequest.StubSize = stubSize; _sectionHeaderTableBuilder->WriteTable(buf, sectionRequest); diff --git a/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp index d3444f0060..76b91f97dc 100644 --- a/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp +++ b/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp @@ -24,12 +24,18 @@ std::vector GuestModuleWriter::WriteLinux(const Relinker::GuestIma } if (std::none_of(headers.begin(), headers.end(), [&](const auto& header) { return header.Type == PT_GNU_STACK; })) headers.push_back({PT_GNU_STACK, PF_R | PF_W, 0, 0, 0, 0, 0, 16}); - if (end > std::numeric_limits::max() - 0x4000) throw Domain::RelinkerException("Guest virtual address overflow"); + std::vector loads; + for (const auto& header : headers) + if (header.Type == 1) loads.push_back(header); + std::stable_sort(loads.begin(), loads.end(), [](const auto& left, const auto& right) { return left.MappedAddress < right.MappedAddress; }); + for (std::size_t index = 0, next = 0; index < headers.size(); ++index) + if (headers[index].Type == 1) headers[index] = loads[next++]; + if (end >std::numeric_limits::max() - 0x4000) throw Domain::RelinkerException("Guest virtual address overflow"); Io::AlignBuffer(bytes, 0x4000); const auto extraOffset = bytes.size(); const auto extraAddress = Io::AlignUp64(end, 0x4000); const auto address = [&] { return extraAddress + bytes.size() - extraOffset; }; - const bool passesModuleArgs = image.Init != 0; + const bool passesModuleArgs = image.HasInit; std::uint64_t argsSlot = 0; std::uint64_t resultSlot = 0; if (passesModuleArgs) { @@ -121,7 +127,7 @@ std::vector GuestModuleWriter::WriteLinux(const Relinker::GuestIma return static_cast(static_cast(distance)); }; const auto lifecycle = [&](std::uint64_t target) { - if (target == 0) return std::uint64_t{}; + if (!image.HasInit) return std::uint64_t{}; const auto start = address(); std::vector stub(kStubSize, 0xc3); stub[0x00] = 0x50; diff --git a/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp index 74e31d19d3..7f8032b78a 100644 --- a/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp +++ b/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp @@ -21,7 +21,7 @@ std::string DynamicName(const Relinker::GuestImage& guest, const std::size_t ind } -std::vector GuestModuleWriter::WriteMacOs(const Relinker::GuestImage& guest, const std::vector& dependencies, const std::string& runPath) const { +std::vector GuestModuleWriter::WriteMacOs(const Relinker::GuestImage& guest, const std::vector& dependencies, const std::string& runPath, const std::function& message) const { using namespace MacOs; Windows::WindowsLoadImage image(guest.Bytes, guest.Headers, false); MacOsImageInput input; @@ -30,6 +30,7 @@ std::vector GuestModuleWriter::WriteMacOs(const Relinker::GuestIma input.Source = &guest.Bytes; input.Headers = &guest.Headers; input.Image = ℑ + input.Message = message; const auto tls = std::find_if(guest.Headers.begin(), guest.Headers.end(), [](const auto& header) { return header.Type == 7; }); std::map targets; @@ -109,7 +110,7 @@ std::vector GuestModuleWriter::WriteMacOs(const Relinker::GuestIma input.RunPaths = MachORunPaths(runPath, "@loader_path"); for (const auto slot : guest.InitArray) input.InitArrayRvas.push_back(image.GetRva(slot, 8)); for (const auto slot : guest.FiniArray) input.FiniArrayRvas.push_back(image.GetRva(slot, 8)); - input.InitRva = guest.Init == 0 ? 0 : image.GetRva(guest.Init); + input.InitRva = guest.HasInit ? image.GetRva(guest.Init) : 0; input.FiniRva = guest.Fini == 0 ? 0 : image.GetRva(guest.Fini); input.Trampolines = &guest.Trampolines; try { diff --git a/core/relinker/elfpatcher/src/macos/MacOsImage.cpp b/core/relinker/elfpatcher/src/macos/MacOsImage.cpp index 61fe2f7d3c..43a00cd7af 100644 --- a/core/relinker/elfpatcher/src/macos/MacOsImage.cpp +++ b/core/relinker/elfpatcher/src/macos/MacOsImage.cpp @@ -33,6 +33,7 @@ constexpr std::uint32_t MhDylib = 6; constexpr std::uint32_t MhNoUndefs = 0x1; constexpr std::uint32_t MhDyldLink = 0x4; constexpr std::uint32_t MhTwoLevel = 0x80; +constexpr std::uint32_t MhWeakDefines = 0x8000; constexpr std::uint32_t LcSegment64 = 0x19; constexpr std::uint32_t LcSymtab = 0x2; @@ -154,6 +155,33 @@ void PadTo(std::vector& out, const std::size_t alignment) { while (out.size() % alignment != 0) out.push_back(0); } +void FitDyldSegmentLimit(std::vector& sections, const std::size_t limit) { + while (sections.size() > limit) { + auto best = sections.end(); + std::uint64_t bestGap = 0; + for (auto section = sections.begin(); std::next(section) != sections.end(); ++section) { + const auto next = std::next(section); + if (section->Name == ".amdstub" || next->Name == ".amdstub" || section->Characteristics != next->Characteristics || + section->Rva + section->MemorySize() != next->Rva) + continue; + const auto gap = next->Rva - section->Rva - section->Data.size(); + if (best == sections.end() || gap < bestGap) { + best = section; + bestGap = gap; + } + } + if (best == sections.end()) return; + const auto next = std::next(best); + const auto memorySize = next->Rva + next->MemorySize() - best->Rva; + if (!next->Data.empty()) { + best->Data.resize(next->Rva - best->Rva); + best->Data.insert(best->Data.end(), next->Data.begin(), next->Data.end()); + } + best->VirtualSize = static_cast(memorySize); + sections.erase(next); + } +} + std::pair Locate(const std::vector& segments, const std::uint64_t rva, const std::uint64_t size) { for (std::size_t index = 0; index < segments.size(); ++index) { const auto& segment = segments[index]; @@ -166,7 +194,9 @@ std::pair Locate(const std::vector& segmen Segment& SegmentAt(std::vector& segments, const std::uint64_t rva, const std::uint64_t size) { for (auto& segment : segments) { - if (rva >= segment.Rva && rva - segment.Rva + size <= segment.Data.size()) return segment; + if (rva < segment.Rva || rva - segment.Rva + size > segment.MemorySize) continue; + if (rva - segment.Rva + size > segment.Data.size()) segment.Data.resize(rva - segment.Rva + size); + return segment; } throw Domain::RelinkerException("Mach-O image patch is outside the image data", rva); } @@ -220,10 +250,10 @@ struct TlsScan { std::vector GsTraps; }; -TlsScan ScanTls(const std::vector& source, const std::vector& headers, const Windows::WindowsLoadImage& image) { +TlsScan ScanTls(const std::vector& source, const std::vector& headers, const Windows::WindowsLoadImage& image, const std::function& message) { TlsScan scan; const Codegen::X64InstructionDecoder decoder; - const auto instructions = Relinker::CodeInstructionCollector().Collect(source, headers); + const auto instructions = Relinker::CodeInstructionCollector().Collect(source, headers, {}, nullptr, message); for (const auto& header : headers) { if (header.Type == 7) { if (scan.Tls != nullptr) throw Domain::RelinkerException("Multiple ELF TLS segments"); @@ -244,7 +274,8 @@ TlsScan ScanTls(const std::vector& source, const std::vector>> Children; - std::optional Rva; + std::optional Export; std::uint64_t Offset = 0; }; @@ -343,8 +374,8 @@ private: rest = {}; } } - if (node->Rva) throw Domain::RelinkerException("Duplicate Mach-O export: " + entry.Symbol); - node->Rva = entry.Rva; + if (node->Export) throw Domain::RelinkerException("Duplicate Mach-O export: " + entry.Symbol); + node->Export = entry; } static void Collect(Node* node, std::vector& order) { @@ -354,9 +385,9 @@ private: static std::vector TerminalBytes(const Node& node) { std::vector bytes; - if (!node.Rva) return bytes; - AppendUleb(bytes, 0); - AppendUleb(bytes, *node.Rva); + if (!node.Export) return bytes; + AppendUleb(bytes, node.Export->Flags); + AppendUleb(bytes, node.Export->Value); return bytes; } @@ -381,8 +412,8 @@ std::vector WriteMacOsImage(MacOsImageInput& input) { auto& image = *input.Image; 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 auto tls = ScanTls(*input.Source, headers, image, input.Message); + 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"); @@ -419,6 +450,7 @@ std::vector WriteMacOsImage(MacOsImageInput& input) { auto stubRva = Windows::AlignRva(image.GetEndRva()); Windows::WindowsTrampolineBuilder().Build(*input.Trampolines, image, sections, stubRva); } + FitDyldSegmentLimit(sections, 16 - guestSegments - 2); std::uint64_t guestEnd = image.GetEndRva(); for (auto& section : sections) { std::uint32_t protection = 0; @@ -481,7 +513,7 @@ std::vector WriteMacOsImage(MacOsImageInput& input) { Io::WriteU64(meta, at, imageBase + descriptor); Io::WriteU64(meta, at + 8, variable.Offset); input.Rebases.push_back(metaRva + static_cast(at)); - input.Exports.push_back({variable.Symbol, metaRva + static_cast(at)}); + input.Exports.push_back({variable.Symbol, metaRva + static_cast(at), variable.Flags}); } const auto codeRva = static_cast(AlignUp(metaRva + metaEnd, PageSize)); Windows::WindowsStubEmitter code(codeRva); @@ -860,7 +892,8 @@ std::vector WriteMacOsImage(MacOsImageInput& input) { Io::WriteU32(file, 12, input.Executable ? MhExecute : MhDylib); Io::WriteU32(file, 16, commandCount); Io::WriteU32(file, 20, static_cast(commands.size())); - Io::WriteU32(file, 24, MhNoUndefs | MhDyldLink | MhTwoLevel); + const bool weakDefinitions = std::any_of(input.Exports.begin(), input.Exports.end(), [](const auto& entry) { return (entry.Flags & ExportWeakDefinition) != 0; }); + Io::WriteU32(file, 24, MhNoUndefs | MhDyldLink | MhTwoLevel | (weakDefinitions ? MhWeakDefines : 0)); std::copy(commands.begin(), commands.end(), file.begin() + HeaderSize); return file; } diff --git a/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp b/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp index 4eb1ee2a4a..ff62ba6e2d 100644 --- a/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp +++ b/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -50,7 +51,7 @@ std::string MachODependency(const std::string& name, const std::string& origin) return "@rpath/" + name; } -MacOsMachOPatcher::MacOsMachOPatcher(const std::optional fixedBase) : fixedBase(fixedBase) { +MacOsMachOPatcher::MacOsMachOPatcher(const std::optional fixedBase, std::function message) : fixedBase(fixedBase), message(std::move(message)) { if (fixedBase && (*fixedBase < 0x20000 || *fixedBase > 0x7fffffff0000 || (*fixedBase & 0xffff) != 0)) throw Domain::RelinkerException("Invalid fixed macOS guest image base"); } @@ -70,6 +71,32 @@ std::vector MacOsMachOPatcher::Patch(std::vector sou input.Source = &sourceElf; input.Headers = &originalHeaders; input.Image = ℑ + input.Message = message; + for (std::size_t offset = 24; offset < dynamicSection.DynSymData.size(); offset += 24) { + const auto& symbols = dynamicSection.DynSymData; + const auto section = Io::ReadU16(symbols, offset + 6); + const auto info = symbols[offset + 4]; + const auto visibility = symbols[offset + 5] & 3; + if (section == SHN_UNDEF || (info >> 4) == 0 || visibility == 1 || visibility == 2) continue; + if (((info >> 4) != STB_GLOBAL && (info >> 4) != STB_WEAK) || + ((info & 15) != STT_NOTYPE && (info & 15) != 1 && (info & 15) != STT_FUNC && (info & 15) != STT_TLS)) + throw Domain::RelinkerException("Unsupported macOS executable export attributes"); + const auto name = "_" + Windows::ReadString(dynamicSection.DynStrData, Io::ReadU32(symbols, offset)); + if (name.size() == 1) throw Domain::RelinkerException("Empty macOS executable export"); + const auto value = Io::ReadU64(symbols, offset + 8); + const auto flags = (info >> 4) == STB_WEAK ? ExportWeakDefinition : std::uint64_t{0}; + if ((info & 15) == STT_TLS) { + if (section >= SHN_LORESERVE) throw Domain::RelinkerException("Unsupported macOS executable TLS export section: " + name); + const auto tls = std::find_if(originalHeaders.begin(), originalHeaders.end(), [](const auto& header) { return header.Type == 7; }); + if (tls != originalHeaders.end() && (value > tls->MemorySize || Io::ReadU64(symbols, offset + 16) > tls->MemorySize - value)) + throw Domain::RelinkerException("macOS executable TLS export exceeds PT_TLS: " + name); + input.TlsExports.push_back({name, value, flags}); + } else if (section == SHN_ABS) input.Exports.push_back({name, value, flags | ExportKindAbsolute}); + else { + if (section >= SHN_LORESERVE) throw Domain::RelinkerException("Unsupported macOS executable export section: " + name); + input.Exports.push_back({name, image.GetRva(value, std::max(Io::ReadU64(symbols, offset + 16), 1)), flags}); + } + } input.Rebases = relocations.BaseRelocations; for (const auto& import : relocations.Imports) input.Binds.push_back({"_" + import.Name, import.TargetRva, static_cast(import.Addend), import.Weak}); diff --git a/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp index a84aea49c4..0b284a4bcd 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp @@ -13,7 +13,7 @@ namespace Elfpatcher { -std::vector GuestModuleWriter::WriteWindows(const Relinker::GuestImage& guest, Domain::GuestRuntime& runtime) const { +std::vector GuestModuleWriter::WriteWindows(const Relinker::GuestImage& guest, Domain::GuestRuntime& runtime, const std::function& message) const { using namespace Windows; if (guest.Symbolic) throw Domain::RelinkerException("DF_SYMBOLIC guest modules are not supported by the Windows writer"); WindowsLoadImage image(guest.Bytes, guest.Headers, false); @@ -84,7 +84,7 @@ std::vector GuestModuleWriter::WriteWindows(const Relinker::GuestI std::array directories{}; std::uint32_t tlsIndex = 0; try { - directories[9] = WindowsTlsBuilder().Build(guest.Bytes, guest.Headers, image, sections, relocations, nextRva, &tlsIndex); + directories[9] = WindowsTlsBuilder().Build(guest.Bytes, guest.Headers, image, sections, relocations, nextRva, &tlsIndex, {}, message); } catch (Domain::RelinkerException& error) { if (error.InputPath.empty()) error.InputPath = Io::Utf8Path(guest.SourcePath); throw; @@ -94,7 +94,7 @@ std::vector GuestModuleWriter::WriteWindows(const Relinker::GuestI if (tlsIndex == 0) throw Domain::RelinkerException("Guest TLS relocation has no TLS block", target); bool written = false; for (auto& section : sections) { - if (rva < section.Rva || rva - section.Rva > section.Data.size() || section.Data.size() - (rva - section.Rva) < 8) continue; + if (!section.Writable(rva, 8)) continue; Io::WriteU64(section.Data, rva - section.Rva, ImageBase + tlsIndex); written = true; break; @@ -150,7 +150,7 @@ std::vector GuestModuleWriter::WriteWindows(const Relinker::GuestI } if (runtime.DeferInitialization) { PeSection lifecycle{".ginit", nextRva, SectionRead | 0x40u, {}}; - Io::AppendU32(lifecycle.Data, guest.Init == 0 ? 0 : image.GetRva(guest.Init)); + Io::AppendU32(lifecycle.Data, guest.HasInit ? image.GetRva(guest.Init) : 0); Io::AppendU32(lifecycle.Data, CheckedRva(guest.InitArray.size())); for (const auto slot : guest.InitArray) Io::AppendU32(lifecycle.Data, image.GetRva(slot, 8)); if (!exports.emplace(Relinker::GuestInitializeExport, nextRva).second) throw Domain::RelinkerException("Duplicate guest export: " + std::string(Relinker::GuestInitializeExport)); @@ -203,7 +203,7 @@ std::vector GuestModuleWriter::WriteWindows(const Relinker::GuestI sections.push_back({".dllmain", entry, SectionRead | SectionExecute | 0x20u, {0xb8, 1, 0, 0, 0, 0xc3}}); for (const auto slot : guest.InitArray) runtime.InitArrayRvas.push_back(image.GetRva(slot, 8)); for (const auto slot : guest.FiniArray) runtime.FiniArrayRvas.push_back(image.GetRva(slot, 8)); - runtime.InitRva = guest.Init == 0 ? 0 : image.GetRva(guest.Init); + runtime.InitRva = guest.HasInit ? image.GetRva(guest.Init) : 0; runtime.UnmatchedImportModules.assign(guest.Dynamic.UnmatchedImportModules.begin(), guest.Dynamic.UnmatchedImportModules.end()); runtime.FiniRva = guest.Fini == 0 ? 0 : image.GetRva(guest.Fini); auto result = WindowsPeWriter().Write(sections, entry, directories); diff --git a/core/relinker/elfpatcher/src/windows/WindowsIconResourceBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsIconResourceBuilder.cpp index 32775ce92c..4a547373bd 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsIconResourceBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsIconResourceBuilder.cpp @@ -16,7 +16,10 @@ std::uint32_t readBigEndian(const std::vector& bytes, const std::s } void validatePng(const std::vector& bytes, const std::filesystem::path& path) { - const auto fail = [&path](const std::string& reason) { throw Domain::RelinkerException("Invalid Windows icon PNG '" + path.string() + "': " + reason); }; + const auto fail = [&path](const std::string& reason) { + const auto utf8 = path.u8string(); + throw Domain::RelinkerException("Invalid Windows icon PNG '" + std::string(utf8.begin(), utf8.end()) + "': " + reason); + }; constexpr std::array signature{0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a}; if (bytes.size() < 33 || !std::equal(signature.begin(), signature.end(), bytes.begin())) fail("missing PNG signature or IHDR"); if (readBigEndian(bytes, 8) != 13 || readBigEndian(bytes, 12) != 0x49484452) fail("invalid IHDR size or type"); @@ -74,6 +77,12 @@ void validatePng(const std::vector& bytes, const std::filesystem:: } PeDirectory WindowsIconResourceBuilder::Build(const std::vector& png, const std::filesystem::path& iconPath, std::vector& sections, const std::uint32_t nextRva) const { + if (nextRva < LoadRva || nextRva % SectionAlignment != 0) + throw Domain::RelinkerException("Invalid Windows resource section RVA", nextRva); + if (std::any_of(sections.begin(), sections.end(), [](const auto& section) { return section.Name == ".rsrc"; })) + throw Domain::RelinkerException("Duplicate Windows resource section"); + if (!sections.empty() && nextRva < AlignRva(static_cast(sections.back().Rva) + sections.back().Data.size())) + throw Domain::RelinkerException("Windows resource section overlaps preceding section", nextRva); if (png.size() > 0x7fffffffu - 184) throw Domain::RelinkerException("Invalid Windows icon size"); validatePng(png, iconPath); std::vector data(184 + png.size()); diff --git a/core/relinker/elfpatcher/src/windows/WindowsLoadImage.cpp b/core/relinker/elfpatcher/src/windows/WindowsLoadImage.cpp index 6fa30dcc54..9f100e655c 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsLoadImage.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsLoadImage.cpp @@ -39,13 +39,10 @@ WindowsLoadImage::WindowsLoadImage(const std::vector& source, cons pageFlags.resize(static_cast(size / SectionAlignment)); for (const auto& segment : segments) { const auto offset = static_cast(segment.MappedAddress - firstAddress); - const auto blockStart = offset / SectionAlignment * SectionAlignment; - const auto blockEnd = static_cast(AlignRva(offset + segment.MemorySize)); - if (blocks.empty() || blocks.back().Offset + blocks.back().Bytes.size() < blockStart) - blocks.push_back({blockStart, {}}); - auto& block = blocks.back(); - block.Bytes.resize(blockEnd - block.Offset); - std::copy_n(source.begin() + static_cast(segment.Offset), static_cast(segment.FileSize), block.Bytes.begin() + static_cast(offset - block.Offset)); + if (segment.FileSize != 0) { + auto& block = backBlock(offset, segment.FileSize); + std::copy_n(source.begin() + static_cast(segment.Offset), static_cast(segment.FileSize), block.Bytes.begin() + static_cast(offset - block.Offset)); + } std::uint32_t flags = 0; if ((segment.Flags & 1) != 0) flags |= SectionExecute | SectionRead; @@ -91,16 +88,30 @@ std::uint64_t WindowsLoadImage::GetImageBase() const { return imageBase; } -WindowsLoadImage::Block& WindowsLoadImage::findBlock(const std::uint64_t offset) { - const auto next = std::upper_bound(blocks.begin(), blocks.end(), offset, [](const std::uint64_t value, const Block& block) { return value < block.Offset; }); - if (next == blocks.begin() || offset - std::prev(next)->Offset >= std::prev(next)->Bytes.size()) - throw Domain::RelinkerException("ELF image offset is not backed by PT_LOAD memory", offset); - return *std::prev(next); +WindowsLoadImage::Block& WindowsLoadImage::backBlock(const std::uint64_t offset, const std::uint64_t length) { + const auto start = offset / SectionAlignment * SectionAlignment; + const auto end = AlignRva(offset + length); + auto index = static_cast(std::upper_bound(blocks.begin(), blocks.end(), offset, [](const std::uint64_t value, const Block& block) { return value < block.Offset; }) - blocks.begin()); + if (index == 0 || blocks[index - 1].Offset + blocks[index - 1].Bytes.size() < start) + blocks.insert(blocks.begin() + static_cast(index), Block{start, {}}); + else + --index; + auto& block = blocks[index]; + if (block.Offset + block.Bytes.size() < end) + block.Bytes.resize(end - block.Offset); + while (index + 1 < blocks.size() && blocks[index + 1].Offset <= block.Offset + block.Bytes.size()) { + const auto& next = blocks[index + 1]; + const auto nextOffset = static_cast(next.Offset - block.Offset); + block.Bytes.resize(std::max(block.Bytes.size(), nextOffset + next.Bytes.size())); + std::copy(next.Bytes.begin(), next.Bytes.end(), block.Bytes.begin() + static_cast(nextOffset)); + blocks.erase(blocks.begin() + static_cast(index + 1)); + } + return block; } void WindowsLoadImage::WritePointer(const std::uint64_t address, const std::uint64_t value) { const auto offset = GetRva(address, 8) - LoadRva; - auto& block = findBlock(offset); + auto& block = backBlock(offset, 8); Io::WriteU64(block.Bytes, offset - block.Offset, value); } @@ -119,18 +130,19 @@ std::vector WindowsLoadImage::BuildSections() const { while (block != blocks.end() && block->Offset + block->Bytes.size() <= first * SectionAlignment) ++block; const auto backed = block != blocks.end() && block->Offset <= first * SectionAlignment; - const auto runEnd = backed ? (block->Offset + block->Bytes.size()) / SectionAlignment : block != blocks.end() ? block->Offset / SectionAlignment : pageFlags.size(); + const auto dataEnd = backed ? static_cast((block->Offset + block->Bytes.size()) / SectionAlignment) : first; + const auto following = backed ? std::next(block) : block; + const auto stop = following != blocks.end() ? following->Offset / SectionAlignment : pageFlags.size(); auto last = first + 1; - while (last < runEnd && pageFlags[last] == pageFlags[first]) + while (last < stop && pageFlags[last] == pageFlags[first]) ++last; const auto characteristics = pageFlags[first] | ((pageFlags[first] & SectionExecute) != 0 ? 0x20u : 0x40u); - const auto rva = CheckedRva(LoadRva + first * SectionAlignment); + PeSection section{".elf" + std::to_string(sections.size()), CheckedRva(LoadRva + first * SectionAlignment), characteristics, {}, CheckedRva((last - first) * SectionAlignment)}; if (backed) { const auto begin = block->Bytes.begin() + static_cast(first * SectionAlignment - block->Offset); - sections.push_back({".elf" + std::to_string(sections.size()), rva, characteristics, {begin, begin + static_cast((last - first) * SectionAlignment)}}); - } else { - sections.push_back({".elf" + std::to_string(sections.size()), rva, characteristics, {}, CheckedRva((last - first) * SectionAlignment)}); + section.Data.assign(begin, begin + static_cast((std::min(last, dataEnd) - first) * SectionAlignment)); } + sections.push_back(std::move(section)); first = last; } return sections; diff --git a/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp b/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp index ef924f15f0..063af7924b 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp @@ -18,7 +18,7 @@ namespace { void writeGotStub(std::vector& sections, const std::uint32_t targetRva, const std::uint32_t stubRva, const std::uint64_t imageBase) { for (auto& section : sections) { - if (section.Data.size() < 8 || targetRva < section.Rva || targetRva - section.Rva > section.Data.size() - 8) + if (!section.Writable(targetRva, 8)) continue; Io::WriteU64(section.Data, targetRva - section.Rva, imageBase + stubRva); return; @@ -28,7 +28,7 @@ void writeGotStub(std::vector& sections, const std::uint32_t targetRv } -WindowsPePatcher::WindowsPePatcher(const bool windowsGui, std::optional> icon, std::filesystem::path iconName, std::function progress, const std::optional fixedBase, const bool traceImports) : progress(std::move(progress)), windowsGui(windowsGui), icon(std::move(icon)), iconName(std::move(iconName)), fixedBase(fixedBase), traceImports(traceImports) { +WindowsPePatcher::WindowsPePatcher(const bool windowsGui, std::optional> icon, std::filesystem::path iconName, std::function progress, const std::optional fixedBase, const bool traceImports, std::function message) : progress(std::move(progress)), windowsGui(windowsGui), icon(std::move(icon)), iconName(std::move(iconName)), fixedBase(fixedBase), traceImports(traceImports), message(std::move(message)) { if (fixedBase && (*fixedBase < 0x20000 || *fixedBase > 0x7fffffff0000 || (*fixedBase & 0xffff) != 0)) throw Domain::RelinkerException("Invalid fixed Windows guest image base"); } @@ -68,7 +68,7 @@ std::vector WindowsPePatcher::Patch(std::vector sour sections.push_back({".ehmeta", nextRva, SectionRead | 0x40u, std::move(metadata)}); nextRva = AlignRva(nextRva + sections.back().Data.size()); } - directories[9] = WindowsTlsBuilder().Build(sourceElf, originalHeaders, image, sections, relocations.BaseRelocations, nextRva, nullptr, [&](auto completed, auto total) { report(1500 + 6500 * completed / total); }); + directories[9] = WindowsTlsBuilder().Build(sourceElf, originalHeaders, image, sections, relocations.BaseRelocations, nextRva, nullptr, [&](auto completed, auto total) { report(1500 + 6500 * completed / total); }, message); report(8000); WindowsTrampolineBuilder().Build(trampolines, image, sections, nextRva); const WindowsImportBuilder importBuilder; diff --git a/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp index e7cd5aec37..71b149d37f 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -25,6 +26,8 @@ PeRelocations WindowsRelocationBuilder::Apply(WindowsLoadImage& image, const Dom const auto symbol = info >> 32; if ((plt && type != 7) || (type != 1 && type != 6 && type != 7 && type != 8 && type != 16 && type != 17 && type != 18)) throw Domain::RelinkerException("Unsupported Windows relocation type " + std::to_string(type), target); + if (target > std::numeric_limits::max() - 8) + throw Domain::RelinkerException("Relocation target address overflow", target); const auto rva = image.GetRva(target, 8); const auto next = ranges.lower_bound(target); if ((next != ranges.end() && next->first < target + 8) || (next != ranges.begin() && std::prev(next)->second > target)) @@ -82,8 +85,14 @@ PeRelocations WindowsRelocationBuilder::Apply(WindowsLoadImage& image, const Dom std::vector WindowsRelocationBuilder::BuildBaseRelocations(const std::vector& targets) const { std::map> pages; - for (const auto target : targets) - pages[target & ~0xfffu].push_back(static_cast(0xa000u | (target & 0xfffu))); + for (const auto target : targets) { + if ((target & 0xfffu) > 0x1000u - 8) + throw Domain::RelinkerException("Base relocation target crosses a page boundary", target); + auto& entries = pages[target & ~0xfffu]; + const auto entry = static_cast(0xa000u | (target & 0xfffu)); + if (std::find(entries.begin(), entries.end(), entry) == entries.end()) + entries.push_back(entry); + } std::vector result; for (const auto& [page, entries] : pages) { Io::AppendU32(result, page); diff --git a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp index edf68e531f..b78a010b40 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp @@ -107,16 +107,16 @@ void trapInstruction(std::vector& sections, const std::uint32_t rva, } -PeDirectory WindowsTlsBuilder::Build(const std::vector& source, const std::vector& headers, const WindowsLoadImage& image, std::vector& sections, std::vector& relocations, std::uint32_t& nextRva, std::uint32_t* tlsIndexRva, const std::function& progress) const { +PeDirectory WindowsTlsBuilder::Build(const std::vector& source, const std::vector& headers, const WindowsLoadImage& image, std::vector& sections, std::vector& relocations, std::uint32_t& nextRva, std::uint32_t* tlsIndexRva, const std::function& progress, const std::function& message) const { const auto report = [&](std::uint64_t completed) { if (progress) progress(completed, 10000); }; const Domain::ProgramHeader* tls = nullptr; const Codegen::X64InstructionDecoder decoder; std::vector accesses; std::vector> gsTraps; - std::set branchTargets; + std::vector branchTargets; std::set entryRoots; - const auto instructions = Relinker::CodeInstructionCollector().Collect(source, headers, [&](auto completed, auto total) { report(7000 * completed / total); }, &entryRoots); + const auto instructions = Relinker::CodeInstructionCollector().Collect(source, headers, [&](auto completed, auto total) { report(7000 * completed / total); }, &entryRoots, message); std::uint64_t scanned = 0; for (const auto& header : headers) { @@ -146,11 +146,12 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co if (info.HasBranchTarget && !info.HasRipRelativeDisp) { const auto target = static_cast(rva) + static_cast(info.Length) + info.BranchDisp; if (target >= 0 && target <= std::numeric_limits::max()) - branchTargets.insert(static_cast(target)); + branchTargets.push_back(static_cast(target)); } if (info.SegmentPrefix == 0x65 && !insidePrevious) { - gsTraps.emplace_back(rva, info.Length); + if (info.HasMemoryOperand) + gsTraps.emplace_back(rva, info.Length); continue; } if (info.SegmentPrefix != 0 && !(insidePrevious && info.SegmentPrefix != 0x64)) { @@ -247,13 +248,13 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co if (displacement != stackGuardDisplacement && (displacement > 0 || displacement < -static_cast(threadPointerOffset))) throw Domain::RelinkerException("Windows guest TLS load displacement " + std::to_string(displacement) + " is not in the thread TLS block, 0 or the stack guard at 0x28", access.FileOffset); } - PeSection data{".gtls", nextRva, SectionRead | SectionWrite | 0x40u, std::vector(templateOffset + blockSize + threadControlBlockSize)}; + PeSection data{".gtls", nextRva, SectionRead | SectionWrite | 0x40u, std::vector(templateOffset + tls->FileSize), CheckedRva(templateOffset + blockSize + threadControlBlockSize)}; const auto indexRva = nextRva + 40; if (tlsIndexRva != nullptr) *tlsIndexRva = indexRva; const auto callbackTableRva = nextRva + 48; const auto templateRva = nextRva + templateOffset; - const auto codeRva = AlignRva(nextRva + data.Data.size()); + const auto codeRva = AlignRva(nextRva + data.MemorySize()); WindowsStubEmitter code(codeRva); const auto loadPointer = [&] { @@ -273,11 +274,14 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co code.PatchBranch(skipCallback, code.GetRva()); code.Emit({0xc3}); + std::sort(branchTargets.begin(), branchTargets.end()); + branchTargets.erase(std::unique(branchTargets.begin(), branchTargets.end()), branchTargets.end()); + std::uint64_t patched = 0; for (const auto& access : accesses) { if (patched % 256 == 0) report(9000 + 900 * patched / accesses.size()); ++patched; - const auto target = branchTargets.upper_bound(access.Rva); + const auto target = std::upper_bound(branchTargets.begin(), branchTargets.end(), access.Rva); if (target != branchTargets.end() && *target < access.Rva + access.Length) throw Domain::RelinkerException("Branch enters a guest TLS instruction", *target); patchAccess(sections, access, code.GetRva()); diff --git a/core/relinker/elfpatcher/src/windows/WindowsTrampolineBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsTrampolineBuilder.cpp index f8d06aa53d..1fcf78fad7 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsTrampolineBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsTrampolineBuilder.cpp @@ -5,18 +5,28 @@ #include #include #include +#include #include namespace Elfpatcher::Windows { namespace { +struct SitePatch { + PeSection* Section = nullptr; + std::ptrdiff_t Offset = 0; + std::size_t Length = 0; + std::vector JumpBytes; +}; + PeSection& findSection(std::vector& sections, const std::uint32_t rva, const std::size_t length) { for (auto& section : sections) { if (rva < section.Rva || rva - section.Rva >= section.Data.size()) continue; if (length > section.Data.size() - (rva - section.Rva)) throw Domain::RelinkerException("AMD-only instruction crosses a PE section boundary", rva); + if ((section.Characteristics & SectionExecute) == 0) + throw Domain::RelinkerException("AMD-only instruction is in a non-executable PE section", rva); return section; } throw Domain::RelinkerException("AMD-only instruction is outside the PE image", rva); @@ -28,12 +38,26 @@ void WindowsTrampolineBuilder::Build(const std::vector& using namespace Codegen::Amd64OnlySubstitutionTable; if (sites.empty()) return; + if (nextRva < LoadRva || nextRva % SectionAlignment != 0) + throw Domain::RelinkerException("Invalid Windows trampoline section RVA", nextRva); + if (std::any_of(sections.begin(), sections.end(), [](const auto& section) { return section.Name == ".amdstub"; })) + throw Domain::RelinkerException("Duplicate Windows trampoline section"); + if (!sections.empty() && nextRva < AlignRva(static_cast(sections.back().Rva) + sections.back().MemorySize())) + throw Domain::RelinkerException("Windows trampoline section overlaps preceding section", nextRva); const auto sectionRva = nextRva; std::vector bytes; + std::map ranges; + std::vector patches; + patches.reserve(sites.size()); for (const auto& site : sites) { const auto siteRva = image.GetRva(site.Address, site.Length); if (site.Length < kJmpRel32.Size || site.OriginalBytes.size() != site.Length || site.Body.size() < kJmpRel32.Size || site.ReturnBranchOffset > site.Body.size() - kJmpRel32.Size || site.Body[site.ReturnBranchOffset] != kJmpRel32.Bytes[0]) throw Domain::RelinkerException("Invalid AMD-only trampoline site", siteRva); + const auto siteEndRva = static_cast(siteRva) + site.Length; + const auto next = ranges.lower_bound(siteRva); + if ((next != ranges.end() && next->first < siteEndRva) || (next != ranges.begin() && std::prev(next)->second > siteRva)) + throw Domain::RelinkerException("Overlapping AMD-only trampoline sites", siteRva); + ranges.emplace(siteRva, siteEndRva); auto& section = findSection(sections, siteRva, site.Length); const auto offset = static_cast(siteRva - section.Rva); if (!std::equal(site.OriginalBytes.begin(), site.OriginalBytes.end(), section.Data.begin() + offset)) @@ -43,19 +67,22 @@ void WindowsTrampolineBuilder::Build(const std::vector& const auto bodyOffset = bytes.size(); bytes.insert(bytes.end(), site.Body.begin(), site.Body.end()); Codegen::ApplyStubRelocations(std::span(bytes.data() + bodyOffset, site.ReturnBranchOffset), site.Relocations, siteRva, stubRva, site.Offset); - const auto returnRva = CheckedRva(siteRva + site.Length); + const auto returnRva = CheckedRva(siteEndRva); const auto displacement = static_cast(returnRva) - (static_cast(stubRva) + static_cast(site.ReturnBranchOffset + kJmpRel32.Size)); if (displacement < std::numeric_limits::min() || displacement > std::numeric_limits::max()) throw Domain::RelinkerException("AMD-only stub return exceeds rel32 range", returnRva); Io::WriteU32(bytes, bodyOffset + site.ReturnBranchOffset + 1, static_cast(displacement)); WindowsStubEmitter jump(siteRva); jump.Rip({0xe9}, stubRva); - const auto jumpBytes = jump.TakeBytes(); - std::fill_n(section.Data.begin() + offset, site.Length, kNop1.Bytes[0]); - std::copy(jumpBytes.begin(), jumpBytes.end(), section.Data.begin() + offset); + patches.push_back({§ion, offset, site.Length, jump.TakeBytes()}); + } + const auto endRva = AlignRva(sectionRva + bytes.size()); + for (const auto& patch : patches) { + std::fill_n(patch.Section->Data.begin() + patch.Offset, patch.Length, kNop1.Bytes[0]); + std::copy(patch.JumpBytes.begin(), patch.JumpBytes.end(), patch.Section->Data.begin() + patch.Offset); } sections.push_back({".amdstub", sectionRva, SectionRead | SectionExecute | 0x20u, std::move(bytes)}); - nextRva = AlignRva(sectionRva + sections.back().Data.size()); + nextRva = endRva; } } 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/ProgramLayoutRangeTests.cpp b/core/relinker/elfpatcher/tests/ProgramLayoutRangeTests.cpp index 458cd8bc2d..18748830de 100644 --- a/core/relinker/elfpatcher/tests/ProgramLayoutRangeTests.cpp +++ b/core/relinker/elfpatcher/tests/ProgramLayoutRangeTests.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -81,9 +82,9 @@ int main() { Require(builder.ComputeExtraBlockVaddr({load}, 0x8800) == 0x3800, "vaddr for end 0x2800, offset 0x8800"); load.MappedAddress = 0x1000; load.MemorySize = 0x1000; Require(builder.ComputeExtraBlockVaddr({load}, 0x1000) == 0x2000, "vaddr for aligned end 0x2000, offset 0x1000"); - Elfpatcher::ProgramHeaderLayoutRequest valid{64, 56, 6, {load}, 0x1000, 0x2000, 0x100, 0x1000, 16, 0x1010, 8}; + Elfpatcher::ProgramHeaderLayoutRequest valid{64, 56, 7, {load}, 0x1000, 0x2000, 0x100, 0x1000, 16, 0x1010, 8}; std::vector output(0x1100, 0xA5); - Require(builder.WriteLayout(output, valid) == 6, "header count for a valid request"); + Require(builder.WriteLayout(output, valid) == 7, "header count for a valid request"); for (int kind = 0; kind < 11; ++kind) { auto request = valid; switch (kind) { @@ -97,7 +98,7 @@ int main() { case 7: request.InterpOffset = maximum; break; case 8: request.ExtraBlockOffset = maximum; break; case 9: request.ExtraBlockSize = 0x1000; break; - case 10: request.PhOff = 0xF00; request.ExtraBlockVaddr = maximum - 0x1fff; break; + case 10: request.PhOff = 0xF00; break; } std::vector bytes(0x1100, 0xA5); const auto before = bytes; @@ -105,7 +106,7 @@ int main() { try { builder.WriteLayout(bytes, request); } catch (const std::exception&) { rejected = true; } Require(rejected && bytes == before, "invalid request accepted or output changed"); } - const std::uint16_t tableCount = 2 + 5; + const std::uint16_t tableCount = 3 + 5; const auto writeLayout = [&builder, tableCount](const std::vector& headers, std::uint64_t extraFileOffset, std::uint64_t extraFileSize) { const std::uint64_t tableSize = tableCount * 56; std::vector bytes(std::max(0xB000, extraFileOffset + tableSize + extraFileSize), 0); @@ -123,6 +124,31 @@ int main() { writeLayout({Load(0x1000, 0x1200, 0x1200, 5), Load(0x2400, 0x1000, 0x1000, 6)}, 0x8A00, 0x400); writeLayout({Load(0x1000, 0x3000, 0x1000, 5), Load(0x3000, 0x4000, 0x1000, 6)}, 0x8A00, 0x400); writeLayout({Load(0x1000, 0x1000, 0x1000, 5), Load(0x2000, 0x1000, 0x0400, 6)}, 0x8800, 0x400); + const std::vector singleLoad{Load(0x1000, 0x1800, 0x1800, 6)}; + Require(builder.RequiredProgramHeaderCount(singleLoad) == 7, "required count must reserve a slot for the synthetic stack"); + const auto writeSingle = [&builder, &singleLoad](std::uint16_t slots) { + std::vector table(0xB000, 0); + Elfpatcher::ProgramHeaderLayoutRequest request{0x8A00, 56, slots, singleLoad, 0x8A00, + builder.ComputeExtraBlockVaddr(singleLoad, 0x8A00), static_cast(slots) * 56 + 0x400, 0x8A00, 0x20, 0x8A20, 8}; + const std::uint16_t written = builder.WriteLayout(table, request); + return std::pair(written, std::move(table)); + }; + const auto [written, table] = writeSingle(builder.RequiredProgramHeaderCount(singleLoad)); + Require(written == 7, "layout must write exactly the required header count"); + std::size_t stacks = 0; + for (std::uint16_t index = 0; index < written; ++index) { + const auto entry = 0x8A00 + index * 56; + if (*reinterpret_cast(table.data() + entry) != Elfpatcher::PT_GNU_STACK) continue; + ++stacks; + Require(*reinterpret_cast(table.data() + entry + 4) == (Elfpatcher::PF_R | Elfpatcher::PF_W) + && *reinterpret_cast(table.data() + entry + 16) == 0 + && *reinterpret_cast(table.data() + entry + 48) == 16, + "synthetic stack must be RW with alignment 16 and no mapping"); + } + Require(stacks == 1, "layout must synthesize exactly one PT_GNU_STACK when the input has none"); + bool stackSlotRejected = false; + try { (void)writeSingle(6); } catch (const std::exception&) { stackSlotRejected = true; } + Require(stackSlotRejected, "layout must refuse to drop the synthetic stack when its slot is missing"); std::cout << "21 program layout cases passed\n"; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; } } diff --git a/core/relinker/elfpatcher/tests/SectionRangeTests.cpp b/core/relinker/elfpatcher/tests/SectionRangeTests.cpp index 7f718b66ce..c75c4e9773 100644 --- a/core/relinker/elfpatcher/tests/SectionRangeTests.cpp +++ b/core/relinker/elfpatcher/tests/SectionRangeTests.cpp @@ -29,7 +29,7 @@ int main() { auto empty = valid; empty.StubOffset = 384; empty.StubSize = 0; bytes = Fixture(); builder.WriteTable(bytes, empty); Require(Io::ReadU64(bytes, Io::ReadU64(bytes, 40) + 5 * 64 + 16) == 0x1100); - for (int kind = 0; kind < 8; ++kind) { + for (int kind = 0; kind < 11; ++kind) { bytes = Fixture(); auto request = valid; switch (kind) { case 0: request.DynStrSize = 257; break; @@ -40,12 +40,15 @@ int main() { case 5: Io::WriteU64(bytes, 32, std::numeric_limits::max()); break; case 6: Io::WriteU64(bytes, 80, std::numeric_limits::max() - 4); break; case 7: Io::WriteU16(bytes, 56, 20); break; + case 8: request.DynStrSize = 16; break; + case 9: bytes.resize(40); break; + case 10: Io::WriteU64(bytes, 32, 48); Io::WriteU32(bytes, 48, 1); Io::WriteU64(bytes, 56, 128); break; } const auto before = bytes; bool rejected = false; - try { builder.WriteTable(bytes, request); } catch (const std::exception&) { rejected = true; } + try { builder.WriteTable(bytes, request); } catch (const std::runtime_error&) { rejected = true; } Require(rejected && bytes == before); } - std::cout << "10 section range cases passed\n"; + std::cout << "13 section range cases passed\n"; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; } } diff --git a/core/relinker/elfpatcher/tests/WindowsDependencyDiagnosticsTests.cpp b/core/relinker/elfpatcher/tests/WindowsDependencyDiagnosticsTests.cpp index 2635ae20e5..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()))) @@ -185,7 +212,7 @@ void expectDiagnostic(const Fs::path& runner, const std::vector& ex if (!CreateProcessW(filename.c_str(), nullptr, nullptr, nullptr, TRUE, CREATE_NO_WINDOW, nullptr, runner.parent_path().wstring().c_str(), &startup, &process)) throw std::runtime_error("Cannot start diagnostic test: " + std::to_string(GetLastError())); CloseHandle(output); - if (WaitForSingleObject(process.hProcess, 20000) != WAIT_OBJECT_0) { + if (WaitForSingleObject(process.hProcess, 120000) != WAIT_OBJECT_0) { TerminateProcess(process.hProcess, 1); throw std::runtime_error("Diagnostic test timed out"); } @@ -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/elfpatcher/tests/WindowsIconResourceTests.cpp b/core/relinker/elfpatcher/tests/WindowsIconResourceTests.cpp index b7aaa68f04..a3813723aa 100644 --- a/core/relinker/elfpatcher/tests/WindowsIconResourceTests.cpp +++ b/core/relinker/elfpatcher/tests/WindowsIconResourceTests.cpp @@ -54,13 +54,14 @@ void checkChunks() { } for (const auto& value : chunks) image.insert(image.end(), value.begin(), value.end()); image.insert(image.end(), Png.end() - 12, Png.end()); - const std::filesystem::path path = std::filesystem::path(name) / "icon.png"; + const std::filesystem::path path = std::filesystem::path(std::u8string(name.begin(), name.end())) / "icon.png"; + const auto utf8 = path.u8string(); std::vector sections; bool rejected = false; try { builder.Build(image, path, sections, 0x10000); } catch (const Domain::RelinkerException& error) { rejected = true; - require(std::string(error.what()).find(path.string()) != std::string::npos, "Icon failure omitted the path"); + require(std::string(error.what()).find(std::string(utf8.begin(), utf8.end())) != std::string::npos, "Icon failure omitted the path"); } require(rejected != valid, "Unexpected validation result for " + name); require(sections.size() == (valid ? 1u : 0u), "Unexpected section mutation for " + name); @@ -119,18 +120,59 @@ void run() { require(readU32(data, 174) == Png.size(), "Unexpected icon size in group entry"); require(std::equal(Png.begin(), Png.end(), data.begin() + 184), "Icon bytes changed"); - const auto badPath = std::filesystem::path("bad/icon0.png"); + const std::filesystem::path badPath(u8"\u65e5\u672c/bad/icon0.png"); + const auto badUtf8 = badPath.u8string(); auto truncated = Png; truncated.resize(40); std::vector badSections; + bool caughtTruncated = false; try { builder.Build(truncated, badPath, badSections, nextRva); } catch (const Domain::RelinkerException& e) { - require(std::string(e.what()).find(badPath.string()) != std::string::npos, std::string("Failure does not name the icon: ") + e.what()); + require(std::string(e.what()).find(std::string(badUtf8.begin(), badUtf8.end())) != std::string::npos, std::string("Failure does not name the icon: ") + e.what()); require(badSections.empty(), "Section added despite the failure"); - return; + caughtTruncated = true; } - throw std::runtime_error("Build succeeded for a malformed icon"); + require(caughtTruncated, "Build succeeded for a malformed icon"); + + for (const auto badRva : {0u, 0x8000u, 0x10001u, 0x10800u}) { + std::vector invalidRvaSections; + bool caught = false; + try { + builder.Build(Png, iconPath, invalidRvaSections, badRva); + } catch (const Domain::RelinkerException& error) { + caught = true; + require(std::string(error.what()).find("Invalid Windows resource section RVA") != std::string::npos, "Unexpected error for bad RVA"); + require(invalidRvaSections.empty(), "Section added on bad RVA"); + } + require(caught, "Build succeeded for invalid resource section RVA"); + } + + std::vector duplicateSections{ + {".rsrc", 0x10000, Elfpatcher::Windows::SectionRead | 0x40u, std::vector(0x1000, 0)} + }; + bool caughtDuplicate = false; + try { + builder.Build(Png, iconPath, duplicateSections, 0x20000); + } catch (const Domain::RelinkerException& error) { + caughtDuplicate = true; + require(std::string(error.what()).find("Duplicate Windows resource section") != std::string::npos, "Unexpected error for duplicate resource section"); + require(duplicateSections.size() == 1, "Section added on duplicate resource section"); + } + require(caughtDuplicate, "Build succeeded with duplicate resource section"); + + std::vector overlappingSections{ + {".text", 0x10000, Elfpatcher::Windows::SectionRead | 0x20u, std::vector(0x50000, 0)} + }; + bool caughtOverlap = false; + try { + builder.Build(Png, iconPath, overlappingSections, 0x50000); + } catch (const Domain::RelinkerException& error) { + caughtOverlap = true; + require(std::string(error.what()).find("Windows resource section overlaps preceding section") != std::string::npos, "Unexpected error for overlapping section"); + require(overlappingSections.size() == 1, "Section added on overlapping section"); + } + require(caughtOverlap, "Build succeeded with overlapping resource section"); } } diff --git a/core/relinker/elfpatcher/tests/WindowsTrampolineBuilderTests.cpp b/core/relinker/elfpatcher/tests/WindowsTrampolineBuilderTests.cpp new file mode 100644 index 0000000000..b710c96f05 --- /dev/null +++ b/core/relinker/elfpatcher/tests/WindowsTrampolineBuilderTests.cpp @@ -0,0 +1,118 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace Elfpatcher::Windows; +using Bytes = std::vector; + +void require(const bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +template +void write(Bytes& bytes, const std::size_t offset, const TValue value) { + std::memcpy(bytes.data() + offset, &value, sizeof(value)); +} + +const Bytes SiteBytes{0xF2, 0x0F, 0x78, 0xDB, 0x08, 0x08}; +const Bytes StubBody{0x90, 0xE9, 0x00, 0x00, 0x00, 0x00}; + +Bytes elfFixture() { + Bytes bytes(0x400); + bytes[0] = 0x7F; + bytes[1] = 'E'; + bytes[2] = 'L'; + bytes[3] = 'F'; + bytes[4] = 2; + bytes[5] = 1; + bytes[6] = 1; + write(bytes, 16, 3); + write(bytes, 18, 62); + write(bytes, 24, 0x1000); + std::fill(bytes.begin() + 0x200, bytes.begin() + 0x300, 0xCC); + std::copy(SiteBytes.begin(), SiteBytes.end(), bytes.begin() + 0x202); + std::copy(SiteBytes.begin(), SiteBytes.end(), bytes.begin() + 0x210); + std::copy(SiteBytes.begin(), SiteBytes.end(), bytes.begin() + 0x302); + return bytes; +} + +std::vector elfHeaders() { + return {{1, 5, 0x200, 0x1000, 0, 0x100, 0x100, 0x1000}, {1, 6, 0x300, 0x2000, 0, 0x100, 0x100, 0x1000}}; +} + +const Bytes Source = elfFixture(); + +Codegen::TrampolineSite site(const std::uint64_t address) { + const auto offset = static_cast(address & 0xFFF) + (address < 0x2000 ? 0x200 : 0x300); + return {static_cast(offset), address, SiteBytes.size(), {Source.begin() + offset, Source.begin() + offset + static_cast(SiteBytes.size())}, StubBody, 1}; +} + +void requireRejected(const WindowsLoadImage& image, const std::vector& sites, std::vector sections, std::uint32_t nextRva, const std::string& expected) { + const auto original = sections; + const auto originalRva = nextRva; + try { + WindowsTrampolineBuilder().Build(sites, image, sections, nextRva); + } catch (const Domain::RelinkerException& error) { + require(std::string(error.what()).find(expected) != std::string::npos, "Unexpected error '" + std::string(error.what()) + "', expected '" + expected + "'"); + require(sections.size() == original.size() && nextRva == originalRva, "Rejected build changed the section list for '" + expected + "'"); + for (std::size_t index = 0; index < sections.size(); ++index) + require(sections[index].Data == original[index].Data, "Rejected build patched section " + sections[index].Name + " for '" + expected + "'"); + return; + } + throw std::runtime_error("Build succeeded, expected '" + expected + "'"); +} + +void run() { + const WindowsLoadImage image(Source, elfHeaders()); + const auto endRva = image.GetEndRva(); + + auto sections = image.BuildSections(); + auto nextRva = endRva; + WindowsTrampolineBuilder().Build({site(0x1002), site(0x1010)}, image, sections, nextRva); + require(sections.size() == 3 && sections.back().Name == ".amdstub" && sections.back().Rva == endRva, "Stub section was not appended"); + require(nextRva == AlignRva(static_cast(endRva) + sections.back().Data.size()), "Next RVA does not follow the stub section"); + require(sections[0].Data[2] == 0xE9 && sections[0].Data[7] == 0x90 && sections[0].Data[0x10] == 0xE9 && sections[0].Data[0x15] == 0x90, "Sites were not replaced by jumps"); + + const auto fresh = image.BuildSections(); + requireRejected(image, {site(0x1002), site(0x1004)}, fresh, endRva, "Overlapping AMD-only trampoline sites"); + requireRejected(image, {site(0x1004), site(0x1002)}, fresh, endRva, "Overlapping AMD-only trampoline sites"); + requireRejected(image, {site(0x1002), site(0x1002)}, fresh, endRva, "Overlapping AMD-only trampoline sites"); + requireRejected(image, {site(0x2002)}, fresh, endRva, "AMD-only instruction is in a non-executable PE section"); + + auto changed = site(0x1010); + changed.OriginalBytes[3] = 0xDC; + requireRejected(image, {site(0x1002), changed}, fresh, endRva, "AMD-only site bytes changed before patching"); + + requireRejected(image, {site(0x1002)}, fresh, endRva + 0x10, "Invalid Windows trampoline section RVA"); + requireRejected(image, {site(0x1002)}, fresh, LoadRva - SectionAlignment, "Invalid Windows trampoline section RVA"); + requireRejected(image, {site(0x1002)}, fresh, endRva - SectionAlignment, "Windows trampoline section overlaps preceding section"); + requireRejected(image, {site(0x1002)}, sections, nextRva, "Duplicate Windows trampoline section"); + + auto untouched = image.BuildSections(); + auto untouchedRva = endRva; + WindowsTrampolineBuilder().Build({}, image, untouched, untouchedRva); + require(untouched.size() == fresh.size() && untouchedRva == endRva, "Empty site list changed the image"); +} + +} + +int main() { + try { + run(); + } catch (const std::exception& e) { + std::cerr << e.what() << '\n'; + return 1; + } + return 0; +} diff --git a/core/relinker/io/include/io/FileIdentity.hpp b/core/relinker/io/include/io/FileIdentity.hpp new file mode 100644 index 0000000000..e33cfc5337 --- /dev/null +++ b/core/relinker/io/include/io/FileIdentity.hpp @@ -0,0 +1,12 @@ +#ifndef IO_FILEIDENTITY_HPP +#define IO_FILEIDENTITY_HPP + +#include + +namespace Io { + +bool SameFile(const std::filesystem::path& first, const std::filesystem::path& second); + +} + +#endif diff --git a/core/relinker/io/src/FileIdentity.cpp b/core/relinker/io/src/FileIdentity.cpp new file mode 100644 index 0000000000..351058935a --- /dev/null +++ b/core/relinker/io/src/FileIdentity.cpp @@ -0,0 +1,54 @@ +#include +#include + +#ifdef _WIN32 +#include +#include +#include +#include +#include +#include +#endif + +namespace Io { + +#ifdef _WIN32 +namespace { + +using Identity = std::pair>; + +Identity FileIdentity(const std::filesystem::path& path) { + const auto handle = CreateFileW(path.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (handle == INVALID_HANDLE_VALUE) + throw std::filesystem::filesystem_error("Cannot open file identity", path, std::error_code(static_cast(GetLastError()), std::system_category())); + Identity identity{}; + FILE_ID_INFO idInformation{}; + BY_HANDLE_FILE_INFORMATION information{}; + bool read = GetFileInformationByHandleEx(handle, FileIdInfo, &idInformation, sizeof(idInformation)) != 0; + if (read) { + identity.first = idInformation.VolumeSerialNumber; + std::copy(std::begin(idInformation.FileId.Identifier), std::end(idInformation.FileId.Identifier), identity.second.begin()); + } else { + read = GetFileInformationByHandle(handle, &information) != 0; + identity.first = information.dwVolumeSerialNumber; + const std::uint64_t index = (static_cast(information.nFileIndexHigh) << 32) | information.nFileIndexLow; + std::memcpy(identity.second.data(), &index, sizeof(index)); + } + const auto error = GetLastError(); + CloseHandle(handle); + if (!read) throw std::filesystem::filesystem_error("Cannot read file identity", path, std::error_code(static_cast(error), std::system_category())); + return identity; +} + +} +#endif + +bool SameFile(const std::filesystem::path& first, const std::filesystem::path& second) { +#ifdef _WIN32 + return FileIdentity(first) == FileIdentity(second); +#else + return std::filesystem::equivalent(first, second); +#endif +} + +} diff --git a/core/relinker/io/tests/FileIdentityTests.cpp b/core/relinker/io/tests/FileIdentityTests.cpp new file mode 100644 index 0000000000..41e6bdc5e7 --- /dev/null +++ b/core/relinker/io/tests/FileIdentityTests.cpp @@ -0,0 +1,64 @@ +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +void write(const std::filesystem::path& path, const std::string& text) { + std::ofstream(path, std::ios::binary) << text; + require(std::filesystem::is_regular_file(path), "Cannot create " + path.string()); +} + +void requireMissing(const std::filesystem::path& first, const std::filesystem::path& second) { + try { + Io::SameFile(first, second); + } catch (const std::filesystem::filesystem_error&) { + return; + } + throw std::runtime_error("SameFile accepted a missing file"); +} + +void run(const std::filesystem::path& directory) { + const auto first = directory / "first.bin"; + const auto link = directory / "link.bin"; + const auto copy = directory / "copy.bin"; + const auto folder = directory / "folder"; + write(first, "same"); + write(copy, "same"); + std::filesystem::create_hard_link(first, link); + std::filesystem::create_directory(folder); + require(Io::SameFile(first, first), "A file differs from itself"); + require(Io::SameFile(first, folder / ".." / "first.bin"), "Two spellings of one file differ"); + require(Io::SameFile(first, link), "A hard link differs from its target"); + require(!Io::SameFile(first, copy), "A copy with the same contents is the same file"); + require(Io::SameFile(folder, directory / "folder" / "."), "Two spellings of one directory differ"); + require(!Io::SameFile(first, folder), "A file is the same as a directory"); + requireMissing(directory / "missing.bin", first); + requireMissing(first, directory / "missing.bin"); +} + +} + +int main() { + const auto directory = std::filesystem::temp_directory_path() / + ("anyps5-file-identity-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + try { + std::filesystem::create_directories(directory); + run(directory); + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + std::error_code ignored; + std::filesystem::remove_all(directory, ignored); + return 1; + } + std::filesystem::remove_all(directory); + return 0; +} diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index a2654c111e..33240459b0 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -75,8 +76,6 @@ ConvertedExecutable Convert(const Cli::Args& args, const std::string& inputPath, return {inputPath, absPath, std::move(converted.Analysis), std::move(converted.Modules), std::move(converted.Executable), std::move(converted.WindowsDiagnostics), std::move(converted.AppliedPatches)}; } -#ifdef _WIN32 - class OutputIdentityDirectory { public: OutputIdentityDirectory(const OutputIdentityDirectory&) = delete; @@ -106,11 +105,9 @@ public: } } - std::filesystem::path GetPath() const { return directory / "file"; } const std::filesystem::path& GetDirectory() const { return directory; } void Remove() { - std::filesystem::remove(GetPath()); std::filesystem::remove(directory); directory.clear(); } @@ -119,28 +116,8 @@ private: std::filesystem::path directory; }; -#endif - bool SameOutputFile(const std::filesystem::path& first, const std::filesystem::path& second) { - if (std::filesystem::equivalent(first, second)) return true; -#ifdef _WIN32 - if (std::filesystem::status(first).permissions() != std::filesystem::status(second).permissions() && - std::filesystem::hard_link_count(first) > 1 && std::filesystem::hard_link_count(second) > 1) { - for (const auto& path : {first, second}) { - if ((std::filesystem::status(path).permissions() & std::filesystem::perms::owner_write) == std::filesystem::perms::none) - throw Domain::RelinkerException("Cannot compare read-only output identities: " + Io::Utf8Path(path)); - } - OutputIdentityDirectory firstDirectory(first.parent_path()); - OutputIdentityDirectory secondDirectory(second.parent_path()); - std::filesystem::create_hard_link(first, firstDirectory.GetPath()); - std::filesystem::create_hard_link(second, secondDirectory.GetPath()); - const bool same = std::filesystem::equivalent(firstDirectory.GetPath(), secondDirectory.GetPath()); - secondDirectory.Remove(); - firstDirectory.Remove(); - return same; - } -#endif - return false; + return Io::SameFile(first, second); } bool SameOutputDestination(const std::filesystem::path& first, const std::filesystem::path& second) { @@ -164,6 +141,18 @@ bool SameOutputDestination(const std::filesystem::path& first, const std::filesy probe.Remove(); return same; } +#else + if (std::filesystem::exists(firstPath.parent_path()) && std::filesystem::exists(secondPath.parent_path()) && + std::filesystem::equivalent(firstPath.parent_path(), secondPath.parent_path())) { + OutputIdentityDirectory probe(firstPath.parent_path()); + const auto name = probe.GetDirectory() / firstPath.filename(); + const auto alternate = probe.GetDirectory() / secondPath.filename(); + std::filesystem::create_directory(name); + const bool same = std::filesystem::exists(alternate) && std::filesystem::equivalent(name, alternate); + std::filesystem::remove(name); + probe.Remove(); + return same; + } #endif return false; } @@ -297,10 +286,10 @@ int Run(const int argc, char* argv[]) { const auto relative = ExecutableRelative(executable); const auto input = ResolveExtraExecutable(moduleRoot, moduleDirectories, relative, executable); const auto output = Relinker::GamePatch::OutputExecutablePath(Io::NativePath(absPath), executable); - if (std::filesystem::equivalent(Io::NativePath(args.inputPath), input)) throw Domain::RelinkerException("Executable is converted twice: " + executable); + if (Io::SameFile(Io::NativePath(args.inputPath), input)) throw Domain::RelinkerException("Executable is converted twice: " + executable); if (output.lexically_normal() == Io::NativePath(absPath).lexically_normal()) throw Domain::RelinkerException("Executable outputs collide: " + Io::Utf8Path(output)); for (const auto& extra : extraExecutables) { - if (std::filesystem::equivalent(extra.Input, input)) throw Domain::RelinkerException("Executable is converted twice: " + executable); + if (Io::SameFile(extra.Input, input)) throw Domain::RelinkerException("Executable is converted twice: " + executable); if (extra.Output.lexically_normal() == output.lexically_normal()) throw Domain::RelinkerException("Executable outputs collide: " + Io::Utf8Path(output)); } extraExecutables.push_back({executable, relative, input, output}); diff --git a/core/relinker/relinker/include/relinker/analysis/CodeInstructionCollector.hpp b/core/relinker/relinker/include/relinker/analysis/CodeInstructionCollector.hpp index de93097012..cdd140af64 100644 --- a/core/relinker/relinker/include/relinker/analysis/CodeInstructionCollector.hpp +++ b/core/relinker/relinker/include/relinker/analysis/CodeInstructionCollector.hpp @@ -4,12 +4,13 @@ #include #include #include +#include namespace Relinker { class CodeInstructionCollector { public: - std::set Collect(const std::vector& bytes, const std::vector& headers, const std::function& progress = {}, std::set* entryRoots = nullptr) const; + std::set Collect(const std::vector& bytes, const std::vector& headers, const std::function& progress = {}, std::set* entryRoots = nullptr, const std::function& message = {}) const; }; } diff --git a/core/relinker/relinker/include/relinker/analysis/RelativeJumpTables.hpp b/core/relinker/relinker/include/relinker/analysis/RelativeJumpTables.hpp index 3e8bc10152..b80aae4651 100644 --- a/core/relinker/relinker/include/relinker/analysis/RelativeJumpTables.hpp +++ b/core/relinker/relinker/include/relinker/analysis/RelativeJumpTables.hpp @@ -16,7 +16,7 @@ struct RelativeJumpTable { VirtualAddress End; }; -std::size_t FindTrailingRelativeJumpTable(std::span code); +std::size_t FindTrailingRelativeJumpTable(std::span code, VirtualAddress codeAddress = 0, std::size_t* tableEnd = nullptr, std::size_t unwindEnd = 0); std::vector FindRelativeJumpTables(std::span segment, VirtualAddress segmentVAddr, const std::vector>& functions); } 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/include/relinker/guest/GuestImage.hpp b/core/relinker/relinker/include/relinker/guest/GuestImage.hpp index f49bd4c0ae..6be45157af 100644 --- a/core/relinker/relinker/include/relinker/guest/GuestImage.hpp +++ b/core/relinker/relinker/include/relinker/guest/GuestImage.hpp @@ -52,6 +52,7 @@ struct GuestImage { std::vector InitArray; std::vector FiniArray; std::uint64_t Init = 0; + bool HasInit = false; std::uint64_t Fini = 0; std::uint64_t Got = 0; bool UsePlatformTlsResolver = true; 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 2b64499909..645dd46f3a 100644 --- a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp +++ b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -12,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -38,7 +40,7 @@ public: } -std::set CodeInstructionCollector::Collect(const std::vector& bytes, const std::vector& headers, const std::function& progress, std::set* entryRoots) const { +std::set CodeInstructionCollector::Collect(const std::vector& bytes, const std::vector& headers, const std::function& progress, std::set* entryRoots, const std::function& message) const { std::uint64_t lastProgress = 0; const auto report = [&](std::uint64_t completed) { if (completed <= lastProgress) return; @@ -96,21 +98,34 @@ std::set CodeInstructionCollector::Collect(const std::ve range(offset, size); return offset; }; + std::vector sceFunctions; + const auto trimToCode = [&](Domain::ProgramHeader& header, std::uint64_t end, bool interpreterAnywhere) { + const std::span segment(bytes.data() + header.Offset, header.FileSize); + auto begin = header.MappedAddress; + for (const auto& interpreter : headers) + if (interpreter.Type == 3 && interpreter.MappedAddress < end && + (interpreterAnywhere ? interpreter.MappedAddress >= begin : interpreter.MappedAddress == begin)) + begin = interpreter.MappedAddress + std::min(interpreter.FileSize, end - interpreter.MappedAddress); + const std::string_view loaderPath("/libexec/ld-elf.so.1", 21); + if (begin == header.MappedAddress && end - begin > loaderPath.size() && std::equal(loaderPath.begin(), loaderPath.end(), segment.begin())) begin += loaderPath.size(); + header.Offset += begin - header.MappedAddress; + header.MappedAddress = begin; + header.FileSize = end - begin; + }; if (sceBase) { const auto unwindFunctions = UnusedNidFilter::ReadExceptionFunctions(bytes, headers, {}, {}); + for (const auto& function : unwindFunctions) sceFunctions.push_back({function.Begin, function.End}); + sceFunctions = Codegen::MergeCodeRanges(std::move(sceFunctions)); for (auto& header : codeHeaders) { if (header.Type != 1 || (header.Flags & 1) == 0) continue; range(header.Offset, header.FileSize); const std::span segment(bytes.data() + header.Offset, header.FileSize); - const auto end = FindSceCodeEnd(segment, header.MappedAddress, unwindFunctions, value(3, 0x61000027).value_or(0), value(2, 0x6100002d).value_or(0)).value_or(header.MappedAddress + header.FileSize); - auto begin = header.MappedAddress; - for (const auto& interpreter : headers) - if (interpreter.Type == 3 && interpreter.MappedAddress >= begin && interpreter.MappedAddress < end) begin = interpreter.MappedAddress + std::min(interpreter.FileSize, end - interpreter.MappedAddress); - const std::string_view loaderPath("/libexec/ld-elf.so.1", 21); - if (begin == header.MappedAddress && end - begin > loaderPath.size() && std::equal(loaderPath.begin(), loaderPath.end(), segment.begin())) begin += loaderPath.size(); - header.Offset += begin - header.MappedAddress; - header.MappedAddress = begin; - header.FileSize = end - begin; + trimToCode(header, FindSceCodeEnd(segment, header.MappedAddress, unwindFunctions, value(3, 0x61000027).value_or(0), value(2, 0x6100002d).value_or(0)).value_or(header.MappedAddress + header.FileSize), true); + } + } else { + for (auto& header : codeHeaders) { + if (header.Type != 1 || (header.Flags & 1) == 0) continue; + trimToCode(header, header.MappedAddress + header.FileSize, false); } } std::set roots; @@ -118,10 +133,77 @@ std::set CodeInstructionCollector::Collect(const std::ve std::map tables; const auto inTable = [&](std::uint64_t address) { const auto after = tables.upper_bound(address); return after != tables.begin() && address < std::prev(after)->second; }; const auto addRoot = [&](std::uint64_t address) { if (isCode(address) && !inTable(address)) roots.insert(address); }; + const auto sceFunctionAfter = [&](std::uint64_t address) { + return std::upper_bound(sceFunctions.begin(), sceFunctions.end(), address, [](std::uint64_t value, const Domain::CodeRange& function) { return value < function.Begin; }); + }; + const auto insideSceFunction = [&](std::uint64_t address) { + const auto after = sceFunctionAfter(address); + return after != sceFunctions.begin() && address < std::prev(after)->End; + }; + const auto nearSceFunctionEnd = [&](std::uint64_t address) { + const auto after = sceFunctionAfter(address); + if (after == sceFunctions.begin()) return false; + const auto end = std::prev(after)->End; + return address >= end && address - end < 256; + }; + const auto codeAt = [&](std::uint64_t address) { + for (const auto& header : codeHeaders) + if (header.Type == 1 && (header.Flags & 1) != 0 && address >= header.MappedAddress && address - header.MappedAddress < header.FileSize) + return std::span(bytes.data() + header.Offset + (address - header.MappedAddress), header.FileSize - (address - header.MappedAddress)); + return std::span(); + }; + const auto absoluteFsAccess = [](const std::uint8_t* code, const Codegen::DecodedInstructionInfo& info) { + return info.SegmentPrefix == 0x64 && info.OpcodeOffset + 2 < info.Length && (code[info.OpcodeOffset + 1] & 0xc7) == 0x04 && code[info.OpcodeOffset + 2] == 0x25; + }; + struct GapRun { + bool Transfer = false; + bool AbsoluteFsAccess = false; + }; + const auto decodeGapRun = [&](std::uint64_t address) { + const auto after = sceFunctionAfter(address); + const auto limit = after == sceFunctions.end() ? std::numeric_limits::max() : after->Begin; + const Codegen::X64InstructionDecoder probe; + GapRun run; + for (std::size_t count = 0; count < 256 && address < limit; ++count) { + const auto code = codeAt(address); + if (code.empty()) return run; + const auto available = std::min(code.size(), limit - address); + Codegen::DecodedInstructionInfo info; + try { + info = probe.DecodeInstruction(code.data(), available); + } catch (const Codegen::CodegenException&) { + return run; + } + if (info.Length == 0 || info.Length > available) return run; + if (absoluteFsAccess(code.data(), info)) run.AbsoluteFsAccess = true; + switch (info.FlowKind) { + case Codegen::ControlFlowKind::UnconditionalJump: + case Codegen::ControlFlowKind::Return: + case Codegen::ControlFlowKind::IndirectJump: + case Codegen::ControlFlowKind::Trap: + run.Transfer = true; + return run; + default: + break; + } + address += info.Length; + } + return run; + }; + std::set dataTargets; + const auto addPointerRoot = [&](std::uint64_t address) { + if (sceFunctions.empty() || !isCode(address) || insideSceFunction(address)) { + addRoot(address); + return; + } + const auto run = decodeGapRun(address); + if (run.AbsoluteFsAccess || (address % 16 == 0 && nearSceFunctionEnd(address) && run.Transfer)) addRoot(address); + else dataTargets.insert(address); + }; const auto addAddressTakenRoot = [&](std::uint64_t address, const std::uint8_t* code, const Codegen::DecodedInstructionInfo& info) { if (!info.HasRipRelativeDisp || code[info.OpcodeOffset] != 0x8d || !(info.RexPrefix & 8)) return; const auto displacement = std::bit_cast(Io::ReadU32(bytes, fileOffset(address, info.Length) + info.RipRelativeDispOffset)); - addRoot(address + info.Length + static_cast(displacement)); + addPointerRoot(address + info.Length + static_cast(displacement)); }; const auto addFunction = [&](std::uint64_t begin, std::uint64_t size, bool symbolAlias) { if (size == 0) return; @@ -145,7 +227,8 @@ std::set CodeInstructionCollector::Collect(const std::ve }; range(0, 64); if (const auto entry = Io::ReadU64(bytes, 24); entry != 0) addRoot(entry); - for (const auto tag : {12u, 13u}) if (const auto address = value(tag, 0x60000000 + tag); address && *address != 0) addRoot(*address); + if (const auto init = value(12, 0x6000000c)) addRoot(*init); + if (const auto fini = value(13, 0x6000000d); fini && *fini != 0) addRoot(*fini); const auto symbolBytes = value(0x6100003f, 0xffffffffffffffffull); std::uint64_t symbolCount = 0; if (symbolBytes) { @@ -198,13 +281,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); @@ -218,21 +309,21 @@ std::set CodeInstructionCollector::Collect(const std::ve && addend <= static_cast(std::numeric_limits::max()) && Io::ReadU64(bytes, symbol + 8) == addend; if ((index != 0 && !zeroSectionSymbol && !matchingDefinedSymbol) || !pointers.Values.emplace(slot, addend).second) throw Domain::RelinkerException("Code analysis: invalid relative relocation", slot); - addRoot(addend); + addPointerRoot(addend); } else if (type == 1 || type == 6 || type == 7) { - if (index == 0 || index >= symbolCount) throw Domain::RelinkerException("Code analysis: invalid relocation symbol", slot); + if (index != 0 && index >= symbolCount) throw Domain::RelinkerException("Code analysis: invalid relocation symbol", slot); const auto symbol = symbols + index * 24; - if (Io::ReadU16(bytes, symbol + 6) == 0) importSlots.insert(slot); + if (index == 0 || Io::ReadU16(bytes, symbol + 6) == 0) importSlots.insert(slot); else { const auto target = Io::ReadU64(bytes, symbol + 8) + addend; if (!pointers.Values.emplace(slot, target).second) throw Domain::RelinkerException("Code analysis: duplicate pointer relocation", slot); - addRoot(target); + addPointerRoot(target); } } } }; - 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); @@ -297,7 +388,7 @@ std::set CodeInstructionCollector::Collect(const std::ve } if (info.Length == 0 || info.Length > end - address) throw Domain::RelinkerException("Code analysis: instruction crosses function boundary", address); - instructions.insert(address); + instructions.emplace_hint(instructions.end(), address); addAddressTakenRoot(address, bytes.data() + offset + address - begin, info); if (info.HasBranchTarget && !info.HasRipRelativeDisp) { const auto target = address + info.Length + static_cast(info.BranchDisp); @@ -358,8 +449,7 @@ std::set CodeInstructionCollector::Collect(const std::ve position = crossed; continue; } - const auto opcode = position + info.OpcodeOffset; - if (info.SegmentPrefix == 0x64 && opcode + 2 < next && (text[opcode + 1] & 0xc7) == 0x04 && text[opcode + 2] == 0x25) addRoot(header.MappedAddress + position); + if (absoluteFsAccess(text + position, info)) addRoot(header.MappedAddress + position); afterTransfer = info.FlowKind != Codegen::ControlFlowKind::Sequential && info.FlowKind != Codegen::ControlFlowKind::ConditionalBranch; position = next; } @@ -397,7 +487,7 @@ std::set CodeInstructionCollector::Collect(const std::ve for (const auto root : roots) if (root >= header.MappedAddress && root - header.MappedAddress < header.FileSize) entries.push_back(root); if (entries.empty()) continue; const std::span text(bytes.data() + header.Offset, header.FileSize); - const auto graph = UnusedNidFilter::BuildControlFlowGraph(text, header.MappedAddress, entries.front(), entries, pointers, true, sectionReachable[i], [&](std::uint64_t count) { report(6000 + 2500 * count / std::max(scanBytes, 1)); }, inTable); + const auto graph = UnusedNidFilter::BuildControlFlowGraph(text, header.MappedAddress, entries.front(), entries, pointers, sceFunctions.empty(), sectionReachable[i], [&](std::uint64_t count) { report(6000 + 2500 * count / std::max(scanBytes, 1)); }, inTable); for (const auto address : graph->ReachableVaddrs()) { if (!decodedInstructions.insert(address).second) continue; instructions.insert(address); @@ -411,43 +501,49 @@ std::set CodeInstructionCollector::Collect(const std::ve report(8500); std::uint64_t checked = 0; std::uint64_t previousEnd = 0; + const Domain::ProgramHeader* cachedHeader = nullptr; + const auto findCodeHeader = [&](std::uint64_t addr) -> const Domain::ProgramHeader* { + if (cachedHeader != nullptr && cachedHeader->Type == 1 && (cachedHeader->Flags & 1) != 0 && addr >= cachedHeader->MappedAddress && addr - cachedHeader->MappedAddress < cachedHeader->FileSize) + return cachedHeader; + for (const auto& header : headers) { + if (header.Type == 1 && (header.Flags & 1) != 0 && addr >= header.MappedAddress && addr - header.MappedAddress < header.FileSize) { + cachedHeader = &header; + return cachedHeader; + } + } + return nullptr; + }; for (const auto address : instructions) { if (progress && checked % 4096 == 0) report(8500 + 1499 * checked / instructions.size()); ++checked; if (address < previousEnd) { std::uint64_t spanEnd = previousEnd; try { - for (const auto& header : headers) { - if (header.Type != 1 || (header.Flags & 1) == 0 || address < header.MappedAddress || address - header.MappedAddress >= header.FileSize) continue; - const auto offset = address - header.MappedAddress; - const auto length = decoder.DecodeInstruction(bytes.data() + header.Offset + offset, header.FileSize - offset).Length; - if (length == 0 || length > header.FileSize - offset) break; - spanEnd = std::max(spanEnd, address + length); - break; + if (const auto* header = findCodeHeader(address)) { + const auto offset = address - header->MappedAddress; + const auto length = decoder.DecodeInstruction(bytes.data() + header->Offset + offset, header->FileSize - offset).Length; + if (length != 0 && length <= header->FileSize - offset) + spanEnd = std::max(spanEnd, address + length); } } catch (const Codegen::CodegenException&) { } bool spanHasFsPrefix = false; - for (const auto& header : headers) { - if (header.Type != 1 || (header.Flags & 1) == 0 || address < header.MappedAddress || address - header.MappedAddress >= header.FileSize) continue; - const auto base = header.Offset + address - header.MappedAddress; + if (const auto* header = findCodeHeader(address)) { + const auto base = header->Offset + address - header->MappedAddress; for (auto probe = base; probe < base + (spanEnd - address) && probe < bytes.size(); ++probe) { if (bytes[probe] == 0x64) { spanHasFsPrefix = true; break; } } - break; } if (!spanHasFsPrefix) { const auto previous = instructions.lower_bound(address); if (previous != instructions.begin()) { const auto previousStart = *std::prev(previous); - for (const auto& header : headers) { - if (header.Type != 1 || (header.Flags & 1) == 0 || previousStart < header.MappedAddress || previousStart - header.MappedAddress >= header.FileSize) continue; - const auto base = header.Offset + previousStart - header.MappedAddress; + if (const auto* header = findCodeHeader(previousStart)) { + const auto base = header->Offset + previousStart - header->MappedAddress; if (decoder.DecodeInstruction(bytes.data() + base, bytes.size() - base).SegmentPrefix == 0x64) spanHasFsPrefix = true; - break; } } } @@ -455,15 +551,22 @@ std::set CodeInstructionCollector::Collect(const std::ve previousEnd = spanEnd; continue; } - for (const auto& header : headers) { - if (header.Type != 1 || (header.Flags & 1) == 0 || address < header.MappedAddress || address - header.MappedAddress >= header.FileSize) continue; - const auto offset = address - header.MappedAddress; - previousEnd = address + decoder.DecodeInstruction(bytes.data() + header.Offset + offset, header.FileSize - offset).Length; - break; + if (const auto* header = findCodeHeader(address)) { + const auto offset = address - header->MappedAddress; + previousEnd = address + decoder.DecodeInstruction(bytes.data() + header->Offset + offset, header->FileSize - offset).Length; } } report(10000); - if (entryRoots) *entryRoots = std::move(roots); + std::erase_if(dataTargets, [&](std::uint64_t address) { return roots.contains(address) || inTable(address); }); + if (!dataTargets.empty() && message) { + std::ostringstream text; + text << "Took " << std::dec << dataTargets.size() << " pointer target(s) outside the unwind functions as data, the first at 0x" << std::hex << *dataTargets.begin() << '\n'; + message(text.str()); + } + if (entryRoots) { + *entryRoots = std::move(roots); + entryRoots->insert(dataTargets.begin(), dataTargets.end()); + } return instructions; } diff --git a/core/relinker/relinker/src/analysis/RelativeJumpTables.cpp b/core/relinker/relinker/src/analysis/RelativeJumpTables.cpp index 0ccbc28717..7c99111960 100644 --- a/core/relinker/relinker/src/analysis/RelativeJumpTables.cpp +++ b/core/relinker/relinker/src/analysis/RelativeJumpTables.cpp @@ -24,15 +24,15 @@ bool IsNop(std::span code, std::size_t at, std::size_t lengt return code[position] == 0x0f && position + 1 < end && code[position + 1] == 0x1f; } -bool OnlyPadding(std::span code, std::size_t from, const Codegen::X64InstructionDecoder& decoder) { - for (auto position = from; position < code.size();) { +bool OnlyPadding(std::span code, std::size_t from, std::size_t to, const Codegen::X64InstructionDecoder& decoder) { + for (auto position = from; position < to;) { if (code[position] == 0x00 || code[position] == 0xcc || code[position] == 0x90) { ++position; continue; } try { - const auto length = decoder.DecodeInstruction(code.data() + position, code.size() - position).Length; - if (length == 0 || length > code.size() - position || !IsNop(code, position, length)) return false; + const auto length = decoder.DecodeInstruction(code.data() + position, to - position).Length; + if (length == 0 || length > to - position || !IsNop(code, position, length)) return false; position += length; } catch (const Codegen::CodegenException&) { return false; @@ -69,7 +69,7 @@ std::size_t TablesEnd(std::span code, std::size_t firstTable } -std::size_t FindTrailingRelativeJumpTable(std::span code) { +std::size_t FindTrailingRelativeJumpTable(std::span code, VirtualAddress codeAddress, std::size_t* tableEnd, std::size_t unwindEnd) { const Codegen::X64InstructionDecoder decoder; std::vector loads; std::vector instructionStarts(code.size() + 1, false); @@ -82,7 +82,13 @@ std::size_t FindTrailingRelativeJumpTable(std::span code) { for (const auto& load : loads) { if (load.Target != position || load.At >= lastIndirectJump) continue; const auto end = TablesEnd(code, position, loads, instructionStarts); - if (end != 0 && OnlyPadding(code, end, decoder)) return position; + if (end == 0) continue; + const auto aligned = std::min(code.size(), static_cast(((codeAddress + end + 15) & ~VirtualAddress{15}) - codeAddress)); + const auto checkedTo = std::max(aligned, std::min(code.size(), unwindEnd)); + const bool toRangeEnd = OnlyPadding(code, end, code.size(), decoder); + if (!toRangeEnd && !OnlyPadding(code, end, checkedTo, decoder)) continue; + if (tableEnd) *tableEnd = toRangeEnd ? code.size() : checkedTo; + return position; } } Codegen::DecodedInstructionInfo info; @@ -122,8 +128,9 @@ std::vector FindRelativeJumpTables(std::span inspectEnd && functions[index + 1].first - inspectEnd <= MaxTableGapAfterFunction) inspectEnd = std::min(functions[index + 1].first, segmentEnd); const auto code = segment.subspan(begin - segmentVAddr, inspectEnd - begin); - if (const auto tail = FindTrailingRelativeJumpTable(code); tail != 0 && tail < code.size()) - tables.push_back({begin, begin + tail, inspectEnd}); + std::size_t tableEnd = code.size(); + if (const auto tail = FindTrailingRelativeJumpTable(code, begin, &tableEnd, std::min(end, segmentEnd) - begin); tail != 0 && tail < code.size()) + tables.push_back({begin, begin + tail, begin + tableEnd}); } return tables; } 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 77ee6dd08c..6003cda8e4 100644 --- a/core/relinker/relinker/src/analysis/SyscallScanner.cpp +++ b/core/relinker/relinker/src/analysis/SyscallScanner.cpp @@ -5,10 +5,14 @@ #include #include #include +#include +#include #include #include +#include #include +#include namespace Relinker { @@ -145,8 +149,12 @@ static std::optional ConstantRaxLoad(const std::uint8_t* bytes, c } static std::optional ConstantSyscallNumber(const std::vector& codeSection, const FileByteOffset codeSectionOffset, const RecentInstructions& recent) { + FileByteOffset next = recent.Before(0).Offset; for (std::size_t step = 1; step <= SYSCALL_NUMBER_LOOKBACK && step < recent.count; ++step) { const auto& match = recent.Before(step); + if (match.Offset + match.Length != next) + return std::nullopt; + next = match.Offset; const std::size_t start = static_cast(match.Offset - codeSectionOffset); if (start >= codeSection.size() || match.Length > codeSection.size() - start) @@ -169,28 +177,110 @@ static std::optional ConstantSyscallNumber(const std::vector ConstantRaxSetEndingAt(const std::vector& code, const std::size_t end) { + if (end >= 10 && code[end - 10] == 0x48 && code[end - 9] == 0xB8) { + std::uint64_t value = 0; + std::memcpy(&value, code.data() + end - 8, sizeof(value)); + return value; + } + std::uint32_t immediate = 0; + if (end >= 7 && code[end - 7] == 0x48 && code[end - 6] == 0xC7 && code[end - 5] == 0xC0) { + std::memcpy(&immediate, code.data() + end - 4, sizeof(immediate)); + return static_cast(static_cast(static_cast(immediate))); + } + if (end >= 5 && code[end - 5] == 0xB8) { + std::memcpy(&immediate, code.data() + end - 4, sizeof(immediate)); + return immediate; + } + if (end >= 2 && (code[end - 2] == 0x31 || code[end - 2] == 0x33) && code[end - 1] == 0xC0) + return 0; + return std::nullopt; +} + +struct LowRaxWrite { + std::size_t Length; + std::uint64_t Mask; + std::uint64_t Value; +}; + +static std::optional ImmediateLowRaxWriteEndingAt(const std::vector& code, const std::size_t end) { + if (end >= 2 && code[end - 2] == 0xB0) + return LowRaxWrite{2, 0xFF, code[end - 1]}; + if (end >= 4 && code[end - 4] == 0x66 && code[end - 3] == 0xB8) + return LowRaxWrite{4, 0xFFFF, static_cast(code[end - 2]) | static_cast(code[end - 1]) << 8}; + return std::nullopt; +} + +static bool MovesOtherRegisterThanRax(const std::uint8_t* bytes, const std::size_t length) { + const std::uint8_t rex = length == 3 ? bytes[0] : 0; + if (length == 3 && (rex & 0xF0) != 0x40) + return false; + const std::uint8_t opcode = bytes[length - 2]; + const std::uint8_t modrm = bytes[length - 1]; + if ((modrm & 0xC0) != 0xC0) + return false; + const bool writesRm = opcode == 0x89 || opcode == 0x31; + const bool writesReg = opcode == 0x8B || opcode == 0x33; + if (!writesRm && !writesReg) + return false; + const unsigned written = writesRm ? (modrm & 7u) | ((rex & 1u) << 3) : ((modrm >> 3) & 7u) | ((rex & 4u) << 1); + return written != 0; +} + +static std::optional ConstantSyscallNumberInBytes(const std::vector& code, const std::size_t syscall) { + using Step = std::tuple; + std::vector pending{{syscall, 0, 0}}; + std::set seen(pending.begin(), pending.end()); + const auto visit = [&](const Step& step) { + if (seen.insert(step).second) + pending.push_back(step); + }; + while (!pending.empty()) { + const auto [end, mask, low] = pending.back(); + pending.pop_back(); + if (const auto value = ConstantRaxSetEndingAt(code, end)) + return (*value & ~mask) | low; + if (const auto write = ImmediateLowRaxWriteEndingAt(code, end)) + visit({end - write->Length, mask | write->Mask, low | (write->Value & ~mask)}); + for (const std::size_t length : {std::size_t{2}, std::size_t{3}}) + if (end >= length && MovesOtherRegisterThanRax(code.data() + end - length, length)) + visit({end - length, mask, low}); + } + return std::nullopt; +} + 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; + const auto functions = Codegen::MergeCodeRanges(unwindRanges); + const auto insideFunction = [&](const std::size_t offset) { + const auto after = std::upper_bound(functions.begin(), functions.end(), offset, [](const std::size_t value, const CodeRange& range) { return value < range.Begin; }); + return after != functions.begin() && offset < std::prev(after)->End; + }; + std::size_t skipped = 0; + std::optional firstSkipped; scanner->VisitCodeSection(codeSection, codeSectionOffset, codeSectionSize, [&](const Codegen::InstructionMatch& match) { recent.Push(match); + if (match.Offset < codeSectionOffset) throw RelinkerException("Instruction match precedes code section offset", match.Offset); const std::size_t start = static_cast(match.Offset - codeSectionOffset); + if (start > codeSection.size() || match.Length > codeSection.size() - start) throw RelinkerException("Instruction match exceeds code section", match.Offset); const auto opcodeOffset = Codegen::DecodedInstruction{codeSection.data() + start, match.Length}.OpcodeOffset(); const std::size_t opcode = start + opcodeOffset; const std::size_t spanLimit = std::min(limit, start + static_cast(match.Length)); @@ -209,15 +299,34 @@ std::vector SyscallScanner::ScanCodeSectionForSyscalls(const std::v return; } + std::optional number; + if (kind == ForbiddenInstruction::Invocation) + number = ConstantSyscallNumber(codeSection, codeSectionOffset, recent); + if (!functions.empty() && !number && !insideFunction(start)) { + if (kind == ForbiddenInstruction::Invocation) + number = ConstantSyscallNumberInBytes(codeSection, opcode); + if (!number) { + ++skipped; + if (!firstSkipped) firstSkipped = match.Offset; + return; + } + } + std::ostringstream msg; msg << "Forbidden syscall instruction at code offset 0x" << std::hex << match.Offset; - if (kind == ForbiddenInstruction::Invocation) - if (const auto number = ConstantSyscallNumber(codeSection, codeSectionOffset, recent)) - msg << ": syscall number " << std::dec << *number; + if (number) + msg << ": syscall number " << std::dec << *number; - throw RelinkerException(msg.str(), match.Offset); + RelinkerException error(msg.str(), match.Offset); + error.InputPath = source; + throw error; }); + if (skipped != 0) { + std::ostringstream message; + message << "Skipped " << std::dec << skipped << " syscall byte pattern(s) without a syscall number outside the unwind functions, the first at code offset 0x" << std::hex << *firstSkipped << '\n'; + progress.Message(message.str()); + } progress.Report({Domain::ProgressStage::Syscalls, source, limit, limit}); return trapped; } @@ -228,7 +337,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/analysis/UnusedNidFilter.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter.cpp index 3dbe4ab5c4..3bcedf3e05 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter.cpp @@ -4,6 +4,7 @@ #include #include #include +#include namespace Relinker { @@ -41,7 +42,7 @@ public: std::vector result; for (const auto& ref : nidRefs) - if (index->IsGotSlotAccessed(ref.RelocationAddress)) + if (ref.RelocationTypeValue != Elfpatcher::R_X86_64_GLOB_DAT || index->IsGotSlotAccessed(ref.RelocationAddress)) result.push_back(ref); return result; } diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp index 4ef6bdf112..da812ba58e 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -33,6 +34,8 @@ namespace { void buildGraph(std::span text, VirtualAddress textVaddr, VirtualAddress entryVaddr, const std::vector& extraEntries, const IRelativeRelocationIndex& relativeRelocations, const std::vector* data, bool followCodeAddresses, std::unordered_set& reachable, const std::function& progress, const std::function& isData) { if (text.size() > std::numeric_limits::max() - textVaddr) throw RelinkerException("Text section range exceeds the address space"); + std::size_t codeSize = text.size(); + while (codeSize > 0 && text[codeSize - 1] == 0) --codeSize; const Codegen::X64InstructionDecoder decoder; std::unordered_set visited; std::queue worklist; @@ -47,6 +50,12 @@ void buildGraph(std::span text, VirtualAddress textVaddr, Vi worklist.push(va); }; + auto zeroPaddingFollows = [&](VirtualAddress va) { + const auto offset = static_cast(va - textVaddr); + const auto aligned = (offset + 15) & ~std::size_t{15}; + return aligned != offset && aligned <= text.size() && std::all_of(text.begin() + offset, text.begin() + aligned, [](std::uint8_t value) { return value == 0; }); + }; + auto takeAddress = [&](VirtualAddress va) { if (data == nullptr) return; if (speculative) @@ -68,8 +77,9 @@ void buildGraph(std::span text, VirtualAddress textVaddr, Vi VirtualAddress va = worklist.front(); worklist.pop(); + const VirtualAddress blockEntry = va; - while (va >= textVaddr && va - textVaddr < text.size() && !reachable.contains(va) && !(isData && isData(va))) { + while (va >= textVaddr && va - textVaddr < text.size() && (va == blockEntry || va - textVaddr < codeSize) && !reachable.contains(va) && !(isData && isData(va))) { std::size_t bufOff = static_cast(va - textVaddr); std::size_t available = text.size() - bufOff; if (available == 0) throw RelinkerException("CFG: zero available bytes at target", va); @@ -137,8 +147,10 @@ void buildGraph(std::span text, VirtualAddress textVaddr, Vi } else if (info.HasBranchTarget) { enqueue(nextVaddr + static_cast(info.BranchDisp)); } + fallThrough = !zeroPaddingFollows(nextVaddr); break; case IndirectCall: + fallThrough = !zeroPaddingFollows(nextVaddr); break; case Return: case IndirectJump: diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/GotAccessIndex.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/GotAccessIndex.cpp index e9405ca433..9d1e5a3c2e 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/GotAccessIndex.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/GotAccessIndex.cpp @@ -43,7 +43,10 @@ std::unique_ptr BuildGotAccessIndex( VirtualAddress nextVaddr = va + static_cast(info.Length); std::int32_t disp = 0; std::memcpy(&disp, text.data() + bufOff + info.RipRelativeDispOffset, 4); + if (disp < 0 && nextVaddr < static_cast(-static_cast(disp))) continue; + if (disp > 0 && std::numeric_limits::max() - nextVaddr < static_cast(disp)) continue; VirtualAddress target = nextVaddr + static_cast(static_cast(disp)); + if (info.AddressSize32) target = static_cast(target); accessed.insert(target); } diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/PltCompactor.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/PltCompactor.cpp index c21acd03ba..6632f2e0ae 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/PltCompactor.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/PltCompactor.cpp @@ -22,20 +22,29 @@ CompactedPlt CompactPlt(const std::vector& originalReferences, con if (reference.RelocationTypeValue != 7) continue; if (!originalSlots.contains(reference.RelocationAddress) || !newSlots.emplace(reference.RelocationAddress, result.SlotCount).second) throw RelinkerException("Strict filter: invalid retained PLT slot", reference.RelocationAddress); - reference.RelocationTableOffset = tableOffset + static_cast(result.SlotCount++) * 24; + const auto slotOffset = static_cast(result.SlotCount++) * 24; + if (tableOffset > std::numeric_limits::max() - slotOffset) + throw RelinkerException("Strict filter: PLT relocation table offset overflow", tableOffset); + reference.RelocationTableOffset = tableOffset + slotOffset; } std::unordered_set found; for (std::size_t offset = 0; offset + 16 <= text.size(); ++offset) { if (text[offset] != 0xFF || text[offset + 1] != 0x25 || text[offset + 6] != 0x68 || text[offset + 11] != 0xE9) continue; + if (offset + 6 > std::numeric_limits::max() - textVaddr) continue; + const auto next = textVaddr + offset + 6; std::int32_t displacement; std::memcpy(&displacement, text.data() + offset + 2, sizeof(displacement)); - const auto slot = textVaddr + offset + 6 + static_cast(static_cast(displacement)); + if (displacement < 0 && next < static_cast(-static_cast(displacement))) continue; + if (displacement > 0 && std::numeric_limits::max() - next < static_cast(displacement)) continue; + const auto slot = next + static_cast(static_cast(displacement)); const auto original = originalSlots.find(slot); if (original == originalSlots.end()) continue; std::uint32_t index; std::memcpy(&index, text.data() + offset + 7, sizeof(index)); if (index != original->second) throw RelinkerException("Strict filter: PLT thunk index disagrees with its relocation", textVaddr + offset); found.insert(slot); + if (textOffset > std::numeric_limits::max() - 7 || offset > std::numeric_limits::max() - textOffset - 7) + throw RelinkerException("Strict filter: PLT patch offset overflow", textOffset); if (const auto kept = newSlots.find(slot); kept != newSlots.end()) { std::vector bytes(sizeof(std::uint32_t)); std::memcpy(bytes.data(), &kept->second, bytes.size()); diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/RelativeRelocationIndex.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/RelativeRelocationIndex.cpp index 1d0be749bc..d6a5eb9ffe 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/RelativeRelocationIndex.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/RelativeRelocationIndex.cpp @@ -4,6 +4,7 @@ #include #include #include +#include namespace Relinker::UnusedNidFilter { @@ -90,7 +91,12 @@ void collectRelativeEntries( if (relType != R_X86_64_RELATIVE) continue; - out[static_cast(rOffset)] = static_cast(rAddend); + if (rAddend < 0) + throw RelinkerException("Relative relocation target address is negative", rOffset); + + auto [it, inserted] = out.emplace(static_cast(rOffset), static_cast(rAddend)); + if (!inserted && it->second != static_cast(rAddend)) + throw RelinkerException("Conflicting relative relocation targets for slot", rOffset); } } @@ -155,6 +161,7 @@ std::unique_ptr BuildRelativeRelocationIndex( } std::unordered_map targets; + bool hasDynamic = false; for (std::uint16_t i = 0; i < phCount; ++i) { std::size_t phPos = static_cast(phOff) + i * phEntSize; @@ -162,43 +169,107 @@ std::unique_ptr BuildRelativeRelocationIndex( std::uint32_t type = read32(elfBytes, phPos); if (type != PT_DYNAMIC) continue; + if (hasDynamic) throw RelinkerException("Multiple PT_DYNAMIC segments", phPos); + hasDynamic = true; std::uint64_t segOff = read64(elfBytes, phPos + 8); std::uint64_t segSz = read64(elfBytes, phPos + 32); if (segOff > elfBytes.size() || segSz > elfBytes.size() - segOff) throw RelinkerException("PT_DYNAMIC segment out of bounds"); + if (segSz % 16 != 0) throw RelinkerException("Invalid dynamic segment size", segOff); - VirtualAddress relaVa = 0; - std::uint64_t relaSz = 0; - VirtualAddress jmprelVa = 0; - std::uint64_t pltrelsz = 0; - bool relaSce = false; - bool jmprelSce = false; + std::optional relaVa; + std::optional relaSz; + std::optional jmprelVa; + std::optional pltrelsz; + bool hasSysvRela = false; + bool hasOsRela = false; + bool hasSysvRelaSz = false; + bool hasOsRelaSz = false; + bool hasSysvJmprel = false; + bool hasOsJmprel = false; + bool hasSysvPltrelsz = false; + bool hasOsPltrelsz = false; + bool terminated = false; for (std::uint64_t off = 0; off <= segSz && segSz - off >= 16; off += 16) { std::size_t pos = static_cast(segOff + off); std::int64_t tag = static_cast(read64(elfBytes, pos)); std::uint64_t val = read64(elfBytes, pos + 8); - if (tag == DT_NULL) break; - if (tag == DT_RELA || tag == DT_OS_RELA) { + if (tag == DT_NULL) { + terminated = true; + break; + } + if (tag == DT_RELA) { + if (hasOsRela) throw RelinkerException("Both DT_OS_ and DT_ variants present for DT_RELA"); + if (hasSysvRela && relaVa.value() != val) throw RelinkerException("Conflicting duplicate DT_RELA tags"); + hasSysvRela = true; relaVa = val; - relaSce = tag == DT_OS_RELA; } - if (tag == DT_RELASZ || tag == DT_OS_RELASZ) relaSz = val; - if (tag == DT_JMPREL || tag == DT_OS_JMPREL) { + if (tag == DT_OS_RELA) { + if (hasSysvRela) throw RelinkerException("Both DT_OS_ and DT_ variants present for DT_RELA"); + if (hasOsRela && relaVa.value() != val) throw RelinkerException("Conflicting duplicate DT_OS_RELA tags"); + hasOsRela = true; + relaVa = val; + } + if (tag == DT_RELASZ) { + if (hasOsRelaSz) throw RelinkerException("Both DT_OS_ and DT_ variants present for DT_RELASZ"); + if (hasSysvRelaSz && relaSz.value() != val) throw RelinkerException("Conflicting duplicate DT_RELASZ tags"); + hasSysvRelaSz = true; + relaSz = val; + } + if (tag == DT_OS_RELASZ) { + if (hasSysvRelaSz) throw RelinkerException("Both DT_OS_ and DT_ variants present for DT_RELASZ"); + if (hasOsRelaSz && relaSz.value() != val) throw RelinkerException("Conflicting duplicate DT_OS_RELASZ tags"); + hasOsRelaSz = true; + relaSz = val; + } + if (tag == DT_JMPREL) { + if (hasOsJmprel) throw RelinkerException("Both DT_OS_ and DT_ variants present for DT_JMPREL"); + if (hasSysvJmprel && jmprelVa.value() != val) throw RelinkerException("Conflicting duplicate DT_JMPREL tags"); + hasSysvJmprel = true; jmprelVa = val; - jmprelSce = tag == DT_OS_JMPREL; } - if (tag == DT_PLTRELSZ || tag == DT_OS_PLTRELSZ) pltrelsz = val; + if (tag == DT_OS_JMPREL) { + if (hasSysvJmprel) throw RelinkerException("Both DT_OS_ and DT_ variants present for DT_JMPREL"); + if (hasOsJmprel && jmprelVa.value() != val) throw RelinkerException("Conflicting duplicate DT_OS_JMPREL tags"); + hasOsJmprel = true; + jmprelVa = val; + } + if (tag == DT_PLTRELSZ) { + if (hasOsPltrelsz) throw RelinkerException("Both DT_OS_ and DT_ variants present for DT_PLTRELSZ"); + if (hasSysvPltrelsz && pltrelsz.value() != val) throw RelinkerException("Conflicting duplicate DT_PLTRELSZ tags"); + hasSysvPltrelsz = true; + pltrelsz = val; + } + if (tag == DT_OS_PLTRELSZ) { + if (hasSysvPltrelsz) throw RelinkerException("Both DT_OS_ and DT_ variants present for DT_PLTRELSZ"); + if (hasOsPltrelsz && pltrelsz.value() != val) throw RelinkerException("Conflicting duplicate DT_OS_PLTRELSZ tags"); + hasOsPltrelsz = true; + pltrelsz = val; + } } - collectRelativeEntries(elfBytes, loads, relaVa, relaSz, relaSce, sceTableBase, targets); - collectRelativeEntries(elfBytes, loads, jmprelVa, pltrelsz, jmprelSce, sceTableBase, targets); + if (!terminated) throw RelinkerException("Unterminated dynamic segment", segOff); - break; + if ((hasSysvRela && hasOsRelaSz) || (hasOsRela && hasSysvRelaSz)) + throw RelinkerException("Mismatched DT_RELA and DT_RELASZ tag variants"); + if (relaVa.has_value() != relaSz.has_value()) + throw RelinkerException("Incomplete relocation table metadata"); + + if ((hasSysvJmprel && hasOsPltrelsz) || (hasOsJmprel && hasSysvPltrelsz)) + throw RelinkerException("Mismatched DT_JMPREL and DT_PLTRELSZ tag variants"); + if (jmprelVa.has_value() != pltrelsz.has_value()) + throw RelinkerException("Incomplete PLT relocation table metadata"); + + if (relaVa.has_value()) + collectRelativeEntries(elfBytes, loads, *relaVa, *relaSz, hasOsRela, sceTableBase, targets); + if (jmprelVa.has_value()) + collectRelativeEntries(elfBytes, loads, *jmprelVa, *pltrelsz, hasOsJmprel, sceTableBase, targets); } return std::make_unique(std::move(targets)); } } + diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictReachability.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictReachability.cpp index e4d5d72a93..21667729d7 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictReachability.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictReachability.cpp @@ -43,6 +43,8 @@ public: for (const auto& [slot, target] : input.Pointers) addAddressTaken(target); collectDataPointers(); collectRelativeTables(); + std::sort(addressTaken.begin(), addressTaken.end()); + addressTaken.erase(std::unique(addressTaken.begin(), addressTaken.end()), addressTaken.end()); std::deque pending; std::set live; const auto enqueue = [&](VirtualAddress target) { @@ -75,19 +77,22 @@ private: const StrictReachabilityInput& input; Codegen::X64InstructionDecoder decoder; std::map regions; - std::set addressTaken; + std::vector addressTaken; std::set tableBases; StrictReachabilityResult result; + Region* lastOwner = nullptr; bool isCode(VirtualAddress address) const { return address >= input.TextVaddr && address - input.TextVaddr < input.Text.size(); } Region& owner(VirtualAddress address) { + if (lastOwner != nullptr && address >= lastOwner->Begin && address < lastOwner->End) return *lastOwner; auto position = regions.upper_bound(address); if (position == regions.begin() || std::prev(position)->second.End <= address) throw RelinkerException("Strict filter: code address has no region", address); - return std::prev(position)->second; + lastOwner = &std::prev(position)->second; + return *lastOwner; } Instruction decode(VirtualAddress address, VirtualAddress end) const { @@ -141,7 +146,7 @@ private: } void addAddressTaken(VirtualAddress target) { - if (isCode(target) || input.ImportSlots.contains(target)) addressTaken.insert(target); + if (isCode(target) || input.ImportSlots.contains(target)) addressTaken.push_back(target); } void buildEdges() { diff --git a/core/relinker/relinker/src/guest/GuestImageReader.cpp b/core/relinker/relinker/src/guest/GuestImageReader.cpp index cf296e8b95..a630010692 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,15 +268,15 @@ 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(12)) { + image.Init = tags.at(12); + image.HasInit = true; + } if (tags.contains(13)) image.Fini = tags.at(13); - if (image.Init != 0) mapped(image.Init, 1, 1); + if (image.HasInit) mapped(image.Init, 1, 1); if (image.Fini != 0) mapped(image.Fini, 1, 1); const auto readLifecycleArray = [&](std::uint64_t addressTag, std::uint64_t sizeTag, std::vector& entries) { if (!tags.contains(addressTag) && !tags.contains(sizeTag)) return; @@ -298,7 +310,7 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector }; readLifecycleArray(25, 27, image.InitArray); readLifecycleArray(26, 28, image.FiniArray); - if (tags.contains(33) && tags.at(33) != 0 && image.Init == 0) fail("Guest PREINIT_ARRAY has no module initializer"); + if (tags.contains(33) && tags.at(33) != 0 && !image.HasInit) fail("Guest PREINIT_ARRAY has no module initializer"); image.Bytes = std::move(bytes); return image; } catch (Domain::RelinkerException& error) { diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index 9c59dbccd1..1f9eed1e66 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -99,20 +99,25 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path std::set outputNames; for (const auto& path : paths) { auto image = std::move(discovered.at(path)); - if (image.OutputName.find_first_of("$\r\n") != std::string::npos) throw Domain::RelinkerException("Unsupported guest filename: " + image.OutputName); + const auto fail = [&](const std::string& message) { + Domain::RelinkerException error(message); + error.InputPath = Io::Utf8Path(path); + throw error; + }; + if (image.OutputName.find_first_of("$\r\n") != std::string::npos) fail("Unsupported guest filename: " + image.OutputName); std::string folded = image.OutputName; if (windows) { for (auto& value : folded) { - if (static_cast(value) >= 128 || value == ':' || value == '$') throw Domain::RelinkerException("Unsupported Windows guest filename: " + image.OutputName); + if (static_cast(value) >= 128 || value == ':' || value == '$') fail("Unsupported Windows guest filename: " + image.OutputName); if (value >= 'A' && value <= 'Z') value = static_cast(value + ('a' - 'A')); } } - if (!outputNames.insert(folded).second) throw Domain::RelinkerException("Conflicting guest output filename: " + image.OutputName); + if (!outputNames.insert(folded).second) fail("Conflicting guest output filename: " + image.OutputName); for (const auto& symbol : image.Symbols) { if (symbol.Section == 0 || symbol.Section == AbsoluteSection || (symbol.Info >> 4) == 0 || symbol.Visibility == 1 || symbol.Visibility == 2) continue; auto& providers = exports[symbol.Name]; const bool repeated = !providers.empty() && providers.back().Provider == images.size(); - if (!windows && repeated) throw Domain::RelinkerException("Duplicate guest export after stripping #: " + symbol.Name + " in " + Io::Utf8Path(path) + " and " + Io::Utf8Path(images[providers.front().Provider].SourcePath)); + if (!windows && repeated) fail("Duplicate guest export after stripping #: " + symbol.Name + " in " + Io::Utf8Path(path) + " and " + Io::Utf8Path(images[providers.front().Provider].SourcePath)); if (!repeated) providers.push_back({images.size(), static_cast(symbol.Info & 15)}); if (!windows && providers.size() > 1) for (const auto& provider : providers) sharedExports[symbol.Name].insert(provider.Provider); @@ -223,6 +228,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 +257,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); } @@ -310,7 +322,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path addHost(dependency, windows || !deferred(index)); if (findGuest(dependency) == guestNames.end()) hosts.push_back(dependency); } - if (!windows && !macos && images[index].Init != 0) { + if (!windows && !macos && images[index].HasInit) { if (std::find(hosts.begin(), hosts.end(), "libkernel.prx") == hosts.end()) hosts.push_back("libkernel.prx"); if (!deferred(index) && uniqueHosts.insert("libkernel.prx").second) hostLibraries.push_back("libkernel.prx"); } @@ -349,7 +361,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 = sceImage ? SceUnwindRanges(code, header.MappedAddress, unwindFunctions) : std::vector{}; + 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()) { @@ -370,7 +383,8 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path runtime.Names = {Io::Utf8Path(image.SourcePath.filename()), image.Soname}; runtime.DeferInitialization = deferred(index); std::vector output; - if (windows) output = Elfpatcher::GuestModuleWriter().WriteWindows(image, runtime); + const auto message = [&](std::string_view text) { progress.Message(text); }; + if (windows) output = Elfpatcher::GuestModuleWriter().WriteWindows(image, runtime, message); else { std::string guestRunPath = runPath; const auto relativeRoot = outputDirectory.lexically_relative(destination.lexically_normal()).generic_string(); @@ -386,7 +400,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path needed.push_back("$ORIGIN/" + dependencyPath.lexically_relative(image.SourcePath.parent_path()).generic_string()); } needed.insert(needed.end(), moduleHostLibraries[index].begin(), moduleHostLibraries[index].end()); - output = macos ? Elfpatcher::GuestModuleWriter().WriteMacOs(image, needed, guestRunPath) : Elfpatcher::GuestModuleWriter().WriteLinux(image, needed, guestRunPath); + output = macos ? Elfpatcher::GuestModuleWriter().WriteMacOs(image, needed, guestRunPath, message) : Elfpatcher::GuestModuleWriter().WriteLinux(image, needed, guestRunPath); } if (windows) { for (const auto& [name, provider] : guestNames) { diff --git a/core/relinker/relinker/src/output/CallRegistryWriter.cpp b/core/relinker/relinker/src/output/CallRegistryWriter.cpp index 983450acc3..8878c6496c 100644 --- a/core/relinker/relinker/src/output/CallRegistryWriter.cpp +++ b/core/relinker/relinker/src/output/CallRegistryWriter.cpp @@ -1,4 +1,5 @@ #include +#include #include namespace Relinker { @@ -7,19 +8,57 @@ static std::string _jsonString(const std::string& s) { std::string out; out.reserve(s.size() + 2); out.push_back('"'); - for (char c : s) { - if (c == '"') out += "\\\""; - else if (c == '\\') out += "\\\\"; - else if (c == '\n') out += "\\n"; - else if (c == '\r') out += "\\r"; - else if (c == '\t') out += "\\t"; - else if (static_cast(c) < 0x20) { - constexpr char digits[] = "0123456789abcdef"; - const auto value = static_cast(c); - out += "\\u00"; - out.push_back(digits[value >> 4]); - out.push_back(digits[value & 0x0f]); - } else out.push_back(c); + for (std::size_t i = 0; i < s.size();) { + const auto c = static_cast(s[i]); + if (c < 0x80) { + if (c == '"') out += "\\\""; + else if (c == '\\') out += "\\\\"; + else if (c == '\n') out += "\\n"; + else if (c == '\r') out += "\\r"; + else if (c == '\t') out += "\\t"; + else if (c < 0x20) { + constexpr char digits[] = "0123456789abcdef"; + out += "\\u00"; + out.push_back(digits[c >> 4]); + out.push_back(digits[c & 0x0f]); + } else out.push_back(static_cast(c)); + ++i; + continue; + } + unsigned count; + std::uint32_t codepoint; + std::uint32_t minimum; + if (c >= 0xC2 && c <= 0xDF) { + count = 1; + codepoint = c & 0x1F; + minimum = 0x80; + } else if (c >= 0xE0 && c <= 0xEF) { + count = 2; + codepoint = c & 0x0F; + minimum = 0x800; + } else if (c >= 0xF0 && c <= 0xF4) { + count = 3; + codepoint = c & 0x07; + minimum = 0x10000; + } else { + throw RelinkerException("Invalid UTF-8 lead byte in call registry string"); + } + if (count > s.size() - (i + 1)) { + throw RelinkerException("Truncated UTF-8 sequence in call registry string"); + } + for (unsigned index = 0; index < count; ++index) { + const auto byte = static_cast(s[i + 1 + index]); + if ((byte & 0xC0) != 0x80) { + throw RelinkerException("Invalid UTF-8 continuation byte in call registry string"); + } + codepoint = (codepoint << 6) | (byte & 0x3F); + } + if (codepoint < minimum || codepoint > 0x10FFFF || (codepoint >= 0xD800 && codepoint <= 0xDFFF)) { + throw RelinkerException("Invalid UTF-8 codepoint in call registry string"); + } + out.push_back(static_cast(c)); + out.append(s, i + 1, count); + i += count + 1; } out.push_back('"'); return out; diff --git a/core/relinker/relinker/src/parsing/ElfReader.cpp b/core/relinker/relinker/src/parsing/ElfReader.cpp index bfed4c355a..0fc55afebd 100644 --- a/core/relinker/relinker/src/parsing/ElfReader.cpp +++ b/core/relinker/relinker/src/parsing/ElfReader.cpp @@ -175,6 +175,8 @@ std::vector ElfReader::ReadProgramHeaders() const { if (tls.FileSize > tls.MemorySize || (tls.Alignment > 1 && !std::has_single_bit(tls.Alignment))) throw RelinkerException("Invalid or unsupported ELF TLS layout", tls.Offset); if (tls.FileSize == 0) continue; + if (!_rangeFits(tls.Offset, tls.FileSize, _fileBuffer.size())) + throw RelinkerException("PT_TLS initialization image is outside the file", tls.Offset); const bool covered = std::any_of(headers.begin(), headers.end(), [&](const ProgramHeader& load) { return load.Type == PT_LOAD && tls.MappedAddress >= load.MappedAddress && tls.MappedAddress - load.MappedAddress <= load.MemorySize && tls.FileSize <= load.MemorySize - (tls.MappedAddress - load.MappedAddress); diff --git a/core/relinker/relinker/src/patching/GamePatch.cpp b/core/relinker/relinker/src/patching/GamePatch.cpp index 4eaed00f32..05d334868a 100644 --- a/core/relinker/relinker/src/patching/GamePatch.cpp +++ b/core/relinker/relinker/src/patching/GamePatch.cpp @@ -245,12 +245,18 @@ struct EntryRange { std::string Reason; }; -std::uint64_t HeaderTableEnd(const ElfHeader& header) { - const std::uint64_t table = header.ProgramHeaderOffset + static_cast(header.ProgramHeaderCount) * header.ProgramHeaderEntrySize; - return std::max(64, table); +bool TouchesHeaderArea(const ElfHeader& header, const std::uint64_t offset, const std::uint64_t length) { + const auto overlaps = [offset, length](const std::uint64_t start, const std::uint64_t size) { + return offset < start + size && start < offset + length; + }; + if (overlaps(0, Elfpatcher::kElfHeaderSize)) return true; + if (header.ProgramHeaderCount == 0 || header.ProgramHeaderEntrySize == 0) return false; + const std::uint64_t tableSize = static_cast(header.ProgramHeaderCount) * header.ProgramHeaderEntrySize; + if (tableSize > std::numeric_limits::max() - header.ProgramHeaderOffset) return true; + return overlaps(header.ProgramHeaderOffset, tableSize); } -EntryRange ResolveEntry(const std::vector& segments, const std::uint64_t fileSize, const std::uint64_t headerEnd, const PatchEntry& entry) { +EntryRange ResolveEntry(const std::vector& segments, const std::uint64_t fileSize, const ElfHeader& header, const PatchEntry& entry) { const std::uint64_t length = entry.Expected.size(); for (const auto& segment : segments) { if (segment.Type != Elfpatcher::PT_LOAD) continue; @@ -259,7 +265,7 @@ EntryRange ResolveEntry(const std::vector& segments, const std::u if (delta >= segment.FileSize || length > segment.FileSize - delta) continue; if (segment.Offset > fileSize || segment.FileSize > fileSize - segment.Offset) return {std::nullopt, "PT_LOAD file mapping is out of bounds"}; const PatchedRange range{segment.Offset + delta, length, entry.VirtualAddress}; - if (range.Offset < headerEnd) return {std::nullopt, "range overlaps the ELF header or program header table"}; + if (TouchesHeaderArea(header, range.Offset, length)) return {std::nullopt, "range overlaps the ELF header or program header table"}; return {range, {}}; } return {std::nullopt, "range is not inside the file bytes of one PT_LOAD"}; @@ -267,9 +273,9 @@ EntryRange ResolveEntry(const std::vector& segments, const std::u std::vector ResolveEach(const ElfReader& reader, const std::vector& entries) { const auto segments = reader.ReadProgramHeaders(); - const auto headerEnd = HeaderTableEnd(reader.ReadHeader()); + const auto header = reader.ReadHeader(); std::vector resolved; - for (const auto& entry : entries) resolved.push_back(ResolveEntry(segments, reader.GetFileSize(), headerEnd, entry)); + for (const auto& entry : entries) resolved.push_back(ResolveEntry(segments, reader.GetFileSize(), header, entry)); return resolved; } diff --git a/core/relinker/relinker/src/pipeline/Conversion.cpp b/core/relinker/relinker/src/pipeline/Conversion.cpp index 9b283de919..ad008fa002 100644 --- a/core/relinker/relinker/src/pipeline/Conversion.cpp +++ b/core/relinker/relinker/src/pipeline/Conversion.cpp @@ -127,9 +127,9 @@ ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOpti std::shared_ptr patcher; if (macos) { - patcher = std::make_shared(options.MacosFixedBase); + patcher = std::make_shared(options.MacosFixedBase, [&](std::string_view text) { inputProgress.Message(text); }); } else if (windows) { - patcher = std::make_shared(options.WindowsGui, std::move(input.Icon), input.InputPath.parent_path() / "sce_sys" / "icon0.png", [&](auto completed, auto total) { inputProgress.Report({Domain::ProgressStage::Image, {}, completed, total}); }, options.WindowsFixedBase, options.TraceImports); + patcher = std::make_shared(options.WindowsGui, std::move(input.Icon), input.InputPath.parent_path() / "sce_sys" / "icon0.png", [&](auto completed, auto total) { inputProgress.Report({Domain::ProgressStage::Image, {}, completed, total}); }, options.WindowsFixedBase, options.TraceImports, [&](std::string_view text) { inputProgress.Message(text); }); } else { patcher = std::make_shared( std::make_shared(), @@ -139,7 +139,8 @@ ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOpti ), std::make_shared(byteWriter), byteWriter, - options.LinuxFixedBase + options.LinuxFixedBase, + options.TraceImports ); } 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/ControlFlowGraphTests.cpp b/core/relinker/relinker/tests/ControlFlowGraphTests.cpp index 2d0fb562ec..94a34990c1 100644 --- a/core/relinker/relinker/tests/ControlFlowGraphTests.cpp +++ b/core/relinker/relinker/tests/ControlFlowGraphTests.cpp @@ -86,6 +86,48 @@ void CheckCalls() { RequireOffsets(*indirect, {0, 2, 3}); } +void CheckCallsBeforeZeroPadding() { + const Pointers pointers; + Bytes padded(0x21, 0); + padded[0] = 0xe8; + SetDisplacement(padded, 1, 5, TextAddress + 0x20); + const Bytes next{0x75, 0x64, 0x55, 0x48, 0x89, 0xe5, 0xc3}; + std::copy(next.begin(), next.end(), padded.begin() + 16); + padded[0x20] = 0xc3; + RequireOffsets(*BuildControlFlowGraph(padded, TextAddress, TextAddress, {}, pointers), {0, 0x20}); + RequireOffsets(*BuildControlFlowGraph(padded, TextAddress, TextAddress, {TextAddress + 16}, pointers), {0, 16, 18, 19, 22, 0x20}); + + Bytes indirect(0x11, 0); + indirect[0] = 0xff; + indirect[1] = 0xd0; + indirect[16] = 0xc3; + RequireOffsets(*BuildControlFlowGraph(indirect, TextAddress, TextAddress, {}, pointers), {0}); +} + +void CheckCallsThatStillFallThrough() { + const Pointers pointers; + Bytes boundary(0x13, 0x90); + boundary[11] = 0xe8; + SetDisplacement(boundary, 12, 16, TextAddress + 0x12); + boundary[16] = 0; + boundary[17] = 0; + boundary[18] = 0xc3; + RequireOffsets(*BuildControlFlowGraph(boundary, TextAddress, TextAddress, {}, pointers), {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 16, 18}); + + Bytes partial(0x11, 0x90); + partial[0] = 0xe8; + SetDisplacement(partial, 1, 5, TextAddress + 0x10); + partial[5] = 0; + partial[6] = 0; + partial[7] = 0xc3; + partial[0x10] = 0xc3; + RequireOffsets(*BuildControlFlowGraph(partial, TextAddress, TextAddress, {}, pointers), {0, 5, 7, 0x10}); + + Bytes truncated{0xe8, 0, 0, 0, 0, 0, 0xc3}; + SetDisplacement(truncated, 1, 5, TextAddress + 6); + RequireOffsets(*BuildControlFlowGraph(truncated, TextAddress, TextAddress, {}, pointers), {0, 5, 6}); +} + void CheckRelocatedTransfers() { constexpr auto slot = TextAddress + 0x1000; Bytes text{0xff, 0x15, 0, 0, 0, 0, 0x90, 0xc3, 0xff, 0x25, 0, 0, 0, 0, 0x90, 0x90, 0xc3}; @@ -141,6 +183,44 @@ void CheckSpeculativeRoots() { Require(threw, "Malformed mandatory root was treated as speculative"); } +void CheckTrailingZeroRoots() { + const Pointers pointers; + const Bytes lone{0x00}; + bool threw = false; + try { + BuildControlFlowGraph(lone, TextAddress, TextAddress, {}, pointers); + } catch (const Codegen::CodegenException&) { + threw = true; + } + Require(threw, "A lone zero instruction at the entry was not decoded"); + + const Bytes trailing{0xc3, 0x00}; + threw = false; + try { + BuildControlFlowGraph(trailing, TextAddress, TextAddress, {TextAddress + 1}, pointers); + } catch (const Codegen::CodegenException&) { + threw = true; + } + Require(threw, "A zero instruction at an extra entry was not decoded"); + + const Bytes jump{0xeb, 0x00, 0x00}; + threw = false; + try { + BuildControlFlowGraph(jump, TextAddress, TextAddress, {}, pointers); + } catch (const Codegen::CodegenException&) { + threw = true; + } + Require(threw, "A branch into a truncated zero instruction was not decoded"); + + const Bytes pair{0x00, 0x00}; + const auto complete = BuildControlFlowGraph(pair, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*complete, {0}); + + const Bytes immediate{0xb8, 0x00, 0x00, 0x00, 0x00}; + const auto zeroImmediate = BuildControlFlowGraph(immediate, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*zeroImmediate, {0}); +} + void CheckBorrowedReachability() { const Pointers pointers; const Bytes text{0x90, 0x90, 0xc3, 0x90, 0xc3}; @@ -211,6 +291,57 @@ void CheckRelativeJumpTables() { nonPadding.back() = 0x55; Require(Relinker::FindTrailingRelativeJumpTable(nonPadding) == nonPadding.size(), "Table followed by non-padding bytes was accepted"); + Bytes codeAfter = text; + codeAfter.resize(48, 0xcc); + codeAfter[32] = 0x55; + codeAfter[33] = 0x5d; + codeAfter[34] = 0xc3; + std::size_t tableEnd = 0; + Require(Relinker::FindTrailingRelativeJumpTable(codeAfter, TextAddress, &tableEnd) == tableOffset && tableEnd == 32, "Table padded to 16 bytes before more code was not found"); + const std::vector> codeAfterFunctions{{TextAddress, TextAddress + 9}, {TextAddress + codeAfter.size(), TextAddress + codeAfter.size() + 8}}; + const auto codeAfterTables = Relinker::FindRelativeJumpTables(codeAfter, TextAddress, codeAfterFunctions); + Require(codeAfterTables.size() == 1 && codeAfterTables[0].Begin == TextAddress + tableOffset && codeAfterTables[0].End == TextAddress + 32, "Table before more code did not end at its padding"); + + std::size_t shiftedEnd = 0; + Require(Relinker::FindTrailingRelativeJumpTable(nonPadding, TextAddress + 1, &shiftedEnd) == tableOffset && shiftedEnd == 31, "Table boundary ignored the code address"); + const auto shifted = Relinker::FindRelativeJumpTables(nonPadding, TextAddress + 1, {{TextAddress + 1, TextAddress + 10}, {TextAddress + 33, TextAddress + 41}}); + Require(shifted.size() == 1 && shifted[0].Begin == TextAddress + 17 && shifted[0].End == TextAddress + 32, "Function address was not passed to the table search"); + Require(Relinker::FindTrailingRelativeJumpTable(nonPadding, TextAddress + 8) == tableOffset, "Table ending on a 16-byte boundary was rejected"); + + Bytes padded = text; + padded.resize(48, 0xcc); + const auto paddedTables = Relinker::FindRelativeJumpTables(padded, TextAddress, {{TextAddress, TextAddress + 9}, {TextAddress + 48, TextAddress + 56}}); + Require(paddedTables.size() == 1 && paddedTables[0].End == TextAddress + 48, "Padding to the end of the range was cut at the boundary"); + + Bytes nopAtRangeEnd = text; + const std::uint8_t fourByteNop[]{0x0f, 0x1f, 0x40, 0x00}; + std::memcpy(nopAtRangeEnd.data() + 28, fourByteNop, sizeof(fourByteNop)); + const auto cut = std::span(nopAtRangeEnd).first(31); + Require(Relinker::FindTrailingRelativeJumpTable(cut) == cut.size(), "Padding check read past the inspected range"); + + Bytes longNops = text; + longNops.resize(48); + const std::uint8_t fifteenByteNop[]{0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x2e, 0x0f, 0x1f, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00}; + const std::uint8_t nineByteNop[]{0x66, 0x0f, 0x1f, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00}; + std::memcpy(longNops.data() + 24, fifteenByteNop, sizeof(fifteenByteNop)); + std::memcpy(longNops.data() + 39, nineByteNop, sizeof(nineByteNop)); + std::size_t longNopsEnd = 0; + Require(Relinker::FindTrailingRelativeJumpTable(longNops, TextAddress, &longNopsEnd) == tableOffset && longNopsEnd == 48, "Table followed by a NOP across the boundary was not found"); + Bytes longNopsCodeAfter = longNops; + longNopsCodeAfter.resize(64, 0xcc); + longNopsCodeAfter[48] = 0x55; + longNopsCodeAfter[49] = 0x5d; + longNopsCodeAfter[50] = 0xc3; + Require(Relinker::FindTrailingRelativeJumpTable(longNopsCodeAfter, TextAddress) == longNopsCodeAfter.size(), "Table ending inside a NOP was accepted"); + + Bytes cutShort = text; + cutShort.resize(56, 0xcc); + const std::int32_t cutShortTargets[]{-16, -7, -6, -5, 0x20, 0x20}; + std::memcpy(cutShort.data() + tableOffset, cutShortTargets, sizeof(cutShortTargets)); + cutShort[48] = 0x55; + cutShort[49] = 0x5d; + cutShort[50] = 0xc3; + Require(Relinker::FindRelativeJumpTables(cutShort, TextAddress, {{TextAddress, TextAddress + cutShort.size()}}).empty(), "Table followed by more of its function was accepted"); } } @@ -220,9 +351,12 @@ int main() { CheckFallthrough(); CheckBranches(); CheckCalls(); + CheckCallsBeforeZeroPadding(); + CheckCallsThatStillFallThrough(); CheckRelocatedTransfers(); CheckCodeAddresses(); CheckSpeculativeRoots(); + CheckTrailingZeroRoots(); CheckBorrowedReachability(); CheckAddressOverflow(); CheckHighVirtualAddress(); diff --git a/core/relinker/relinker/tests/ConversionTests.cpp b/core/relinker/relinker/tests/ConversionTests.cpp index 5a5e0b20e3..0b6d9d62c9 100644 --- a/core/relinker/relinker/tests/ConversionTests.cpp +++ b/core/relinker/relinker/tests/ConversionTests.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -232,6 +233,19 @@ void cancellation() { } } +void baseRelocations() { + Elfpatcher::Windows::WindowsRelocationBuilder builder; + const auto relocs = builder.BuildBaseRelocations({0x10000, 0x10000, 0x10008}); + require(relocs.size() == 12, "Base relocation duplicates were not deduplicated"); + require(Io::ReadU32(relocs, 0) == 0x10000, "Wrong base relocation page RVA"); + require(Io::ReadU32(relocs, 4) == 12, "Wrong base relocation block size"); + require(Io::ReadU16(relocs, 8) == 0xa000, "Wrong first relocation entry"); + require(Io::ReadU16(relocs, 10) == 0xa008, "Wrong second relocation entry"); + reject([&] { (void)builder.BuildBaseRelocations({0x10ffc}); }); + reject([&] { (void)builder.BuildBaseRelocations({0x10ffd}); }); + reject([&] { (void)builder.BuildBaseRelocations({0x10fff}); }); +} + } int main() { @@ -241,6 +255,7 @@ int main() { scanning(); diagnosticContext(); cancellation(); + baseRelocations(); } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; diff --git a/core/relinker/relinker/tests/ElfReaderBoundsTests.cpp b/core/relinker/relinker/tests/ElfReaderBoundsTests.cpp index 8b1c17d895..154238155d 100644 --- a/core/relinker/relinker/tests/ElfReaderBoundsTests.cpp +++ b/core/relinker/relinker/tests/ElfReaderBoundsTests.cpp @@ -148,6 +148,29 @@ int main() { const auto headers = Relinker::ElfReader(std::move(bytes)).ReadProgramHeaders(); Require(headers.size() == 2 && headers[0].Type == 1 && headers[1].Type == 7 && headers[1].MappedAddress == 0x1008); }}, + {"TLS image ending at end of file", [] { + auto bytes = TlsFixture(0x1008, 8, 16, 8); + Write(bytes, 0x2a0, 0x2f8); + const auto headers = Relinker::ElfReader(std::move(bytes)).ReadProgramHeaders(); + Require(headers.size() == 2 && headers[1].Offset + headers[1].FileSize == 0x300); + }}, + {"empty TLS image beyond end of file", [] { + constexpr std::uint64_t offsets[] = {0x300, 0x301, std::numeric_limits::max()}; + for (const std::uint64_t offset : offsets) { + auto bytes = TlsFixture(0x9000, 0, 16, 16); + Write(bytes, 0x2a0, offset); + const auto headers = Relinker::ElfReader(std::move(bytes)).ReadProgramHeaders(); + Require(headers.size() == 2 && headers[1].Offset == offset && headers[1].FileSize == 0); + } + }}, + {"TLS file range past end of file", [] { + constexpr std::uint64_t offsets[] = {0x2f9, 0x300, 0x301, std::numeric_limits::max() - 3, std::numeric_limits::max()}; + for (const std::uint64_t offset : offsets) { + auto bytes = TlsFixture(0x1008, 8, 16, 1); + Write(bytes, 0x2a0, offset); + RejectProgramHeaders(std::move(bytes)); + } + }}, {"TLS image outside load memory", [] { constexpr std::uint64_t addresses[] = {0x0ff8, 0x103c, 0x2000, std::numeric_limits::max() - 3}; for (const std::uint64_t address : addresses) { diff --git a/core/relinker/relinker/tests/SdkMmanSyscallTests.cpp b/core/relinker/relinker/tests/SdkMmanSyscallTests.cpp index 1ede68a821..f929c7837b 100644 --- a/core/relinker/relinker/tests/SdkMmanSyscallTests.cpp +++ b/core/relinker/relinker/tests/SdkMmanSyscallTests.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -73,6 +74,20 @@ void CheckFixtures() { Require(Rejected(embedded, true, embedded.size()), "SDK matching accepted a prefix that starts inside an immediate"); for (const auto& code : std::vector{{0xB8, 0xDD, 0x01, 0x00, 0x00, 0x0F, 0x05, 0xC3}, {0xCD, 0x80}, {0x0F, 0x34}, {0x0F, 0x07}}) Require(Rejected(code, true, code.size()), "SDK trap mode accepted an unrelated privileged instruction"); + bool overflowRejected = false; + try { + Relinker::MakeSyscallScanner(testProgress, true)->ScanCodeSectionForSyscalls(Fixtures.front().code, std::numeric_limits::max(), 1); + } catch (const Domain::RelinkerException& error) { + overflowRejected = std::string(error.what()) == "Code section offset overflow"; + } + Require(overflowRejected, "Scanner accepted code section offset overflow"); + bool sizeExceededRejected = false; + try { + Relinker::MakeSyscallScanner(testProgress, true)->ScanCodeSectionForSyscalls(Fixtures.front().code, 0, Fixtures.front().code.size() + 1); + } catch (const Domain::RelinkerException& error) { + sizeExceededRejected = std::string(error.what()) == "Code section exceeds the input buffer"; + } + Require(sizeExceededRejected, "Scanner accepted code section size exceeding input"); std::cout << "SDK inline syscall checks passed, " << rejectedMutations << " near-match rejections\n"; } 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_call_registry.py b/core/relinker/relinker/tests/test_call_registry.py index 1bc1749afe..5b9a61ac30 100644 --- a/core/relinker/relinker/tests/test_call_registry.py +++ b/core/relinker/relinker/tests/test_call_registry.py @@ -8,27 +8,53 @@ import tempfile from test_string_table_bounds import fixture +def make_image(strings, needed_offset=1, symbol_offset=8): + image = fixture(str_size=len(strings), needed_offset=needed_offset, symbol_offset=symbol_offset, table_bytes=strings) + struct.pack_into("\n' + '#include \n' + '#include \n' + '#include \n' + '\n' + 'int main(int argc, char **argv) {\n' + ' if (argc != 5) return 4;\n' + ' void *host = dlopen(argv[1], RTLD_NOW | RTLD_GLOBAL);\n' + ' if (!host) { fprintf(stderr, "%s\\n", dlerror()); return 1; }\n' + ' void (*set)(size_t, const void *) = (void (*)(size_t, const void *))dlsym(host, "SetPendingArgs");\n' + ' if (!set) { fprintf(stderr, "%s\\n", dlerror()); return 2; }\n' + ' set((size_t)strtoull(argv[3], NULL, 0), (const void *)(size_t)strtoull(argv[4], NULL, 0));\n' + ' if (!dlopen(argv[2], RTLD_NOW | RTLD_LOCAL)) { fprintf(stderr, "%s\\n", dlerror()); return 3; }\n' + ' return 0;\n' + '}\n' + ) + result = subprocess.run(['xcrun', 'clang', '-arch', 'x86_64', str(source), '-o', str(loader)], + capture_output=True, text=True, timeout=20) + assert result.returncode == 0, (result.stdout, result.stderr) + return (['/usr/bin/arch', '-x86_64'] if platform.machine() == 'arm64' else []) + [str(loader)] + + +def load_and_record(work, provider, module, loader=None): events = work / 'events.txt' - run = subprocess.run([sys.executable, __file__, '--load', str(provider), str(module)], + command = loader + [str(provider), str(module), hex(ARGS), hex(ARGP)] if loader else [sys.executable, __file__, '--load', str(provider), str(module)] + run = subprocess.run(command, env={**os.environ, 'ANYPS5_GUEST_MODULE_ARGS': str(events)}, capture_output=True, text=True, timeout=30) assert run.returncode == 0, (run.returncode, run.stdout, run.stderr) @@ -135,7 +166,7 @@ def main(): relinker = Path(sys.argv[1]).resolve() host = Path(sys.argv[2]).resolve() linux = sys.platform == 'linux' and os.uname().machine == 'x86_64' - macos = sys.platform == 'darwin' and platform.machine() == 'x86_64' + macos = sys.platform == 'darwin' and macho_x86_64(host) with tempfile.TemporaryDirectory(prefix='anyps5-guest-args-') as directory: work = Path(directory) for deferred in (False, True): @@ -193,7 +224,7 @@ def main(): libraries.mkdir() shutil.copyfile(host, libraries / 'libkernel.prx') (libraries / 'libc.prx').symlink_to('libkernel.prx') - load_and_record(mach, libraries / 'libkernel.prx', mach_converted) + load_and_record(mach, libraries / 'libkernel.prx', mach_converted, macos_loader(work)) else: print('Guest module argument tests passed: native execution skipped') return diff --git a/core/relinker/relinker/tests/test_guest_module_dir.py b/core/relinker/relinker/tests/test_guest_module_dir.py index 74180910de..c80b9ce2b1 100644 --- a/core/relinker/relinker/tests/test_guest_module_dir.py +++ b/core/relinker/relinker/tests/test_guest_module_dir.py @@ -214,6 +214,15 @@ def main(): run = subprocess.run([str(output)], capture_output=True, text=True, timeout=30) assert run.returncode == 42, (run.returncode, run.stdout, run.stderr) + case = work / f"{windows}-root-input-link" + case.mkdir() + (case / "root.prx").write_bytes(module_with_symbol(True)) + (case / "input.elf").write_bytes(main_fixture()) + os.link(case / "input.elf", case / "input-link.exe") + result, output = convert(case, windows, ["--module-dir", "."]) + assert result.returncode == 0, (result.stdout, result.stderr) + assert list((case / "app0").rglob("*.guest.prx")) == [case / "app0" / "root.prx.guest.prx"], list(case.rglob("*.guest.prx")) + case = work / f"{windows}-root-needed" case.mkdir() (case / "needed.prx").write_bytes(module_with_symbol(True)) diff --git a/core/relinker/relinker/tests/test_guest_module_directories.py b/core/relinker/relinker/tests/test_guest_module_directories.py index c0f7188a72..6ea29ae84f 100644 --- a/core/relinker/relinker/tests/test_guest_module_directories.py +++ b/core/relinker/relinker/tests/test_guest_module_directories.py @@ -24,12 +24,16 @@ def module_with_symbol(exported, symbolic=False): return image -def module_with_soname(soname): +def module_with_soname(soname, duplicate_alias=False): image = module_with_symbol(True) - strings = b"\0shared#A#B\0" + soname.encode() + b"\0" + alias = b"shared#C#B\0" if duplicate_alias else b"" + strings = b"\0shared#A#B\0" + alias + soname.encode() + b"\0" image[0x800:0x800 + len(strings)] = strings + if duplicate_alias: + struct.pack_into("= len(image), "The grown program header table must be moved past the input" assert "PT_GNU_EH_FRAME" not in result.stderr, result.stderr assert elf[table_end:table_end + 56] == image[table_end:table_end + 56], "Bytes after the input program header table changed" @@ -113,7 +114,8 @@ def main(): image[table_end:table_end + 56] = bytes([0xa5]) * 56 result, elf = relink(relinker, directory, "no-frame-exact-slots", image) headers = program_headers(elf) - assert len(headers) == 6, headers + assert len(headers) == 7, headers + assert [header for header in headers if header[0] == PT_GNU_STACK] == [(PT_GNU_STACK, 6, 0, 0, 0, 0, 0, 16)], headers assert "PT_GNU_EH_FRAME" not in result.stderr, result.stderr assert elf[table_end:table_end + 56] == image[table_end:table_end + 56], "Bytes after the program header table changed" @@ -122,7 +124,7 @@ def main(): image[64 + 5 * 56:64 + 6 * 56] = bytes([0xa5]) * 56 result, elf = relink(relinker, directory, "few-slots-" + str(with_frame), image) headers = program_headers(elf) - assert len(headers) == (7 if with_frame else 6), headers + assert len(headers) == (8 if with_frame else 7), headers assert [header for header in headers if header[0] == PT_GNU_EH_FRAME] == ([EH_FRAME_HEADER] if with_frame else []), headers assert struct.unpack_from("= len(image), "The grown program header table must be moved past the input" assert elf[64 + 5 * 56:64 + 6 * 56] == image[64 + 5 * 56:64 + 6 * 56], "Bytes after the input program header table changed" @@ -138,7 +140,7 @@ def main(): source.write_bytes(image) result = subprocess.run([str(relinker), "--skip-sce-module", str(source), str(output)], capture_output=True, text=True, timeout=20) assert result.returncode == 2, (result.returncode, result.stdout, result.stderr) - assert "Not enough program header slots: need 65536, available 65534" in result.stderr, result.stderr + assert "Not enough program header slots: need 65537, available 65534" in result.stderr, result.stderr assert not output.exists(), "An unrepresentable header count must not produce an executable" print("Linux eh_frame header test passed") diff --git a/core/relinker/relinker/tests/test_linux_entry_argv.py b/core/relinker/relinker/tests/test_linux_entry_argv.py index 82c043c19f..4dad8c6e07 100644 --- a/core/relinker/relinker/tests/test_linux_entry_argv.py +++ b/core/relinker/relinker/tests/test_linux_entry_argv.py @@ -8,6 +8,7 @@ import subprocess import sys import tempfile +from signal_run import run_without_core_dump from test_linux_load_alignment import PT_LOAD, PT_SCE_VERSION, fixture, loads ENTRY = 0x10 @@ -69,7 +70,7 @@ def main(): for executable in (output, relocated): executable.chmod(0o755) for arguments, expected in ((["Z"], -signal.SIGTRAP), (["Z", "extra"], -signal.SIGILL)): - executed = subprocess.run([str(executable), *arguments], capture_output=True, timeout=20) + executed = run_without_core_dump([str(executable), *arguments], capture_output=True, timeout=20) assert executed.returncode == expected, (executable.name, arguments, executed.returncode) for address, error in ((0x7FF00000, None), (0x80000000, "Entry stub call exceeds rel32 range")): source.write_bytes(far_load_fixture(address)) diff --git a/core/relinker/relinker/tests/test_linux_fixed_base.py b/core/relinker/relinker/tests/test_linux_fixed_base.py index e5904a65f7..84244f4533 100644 --- a/core/relinker/relinker/tests/test_linux_fixed_base.py +++ b/core/relinker/relinker/tests/test_linux_fixed_base.py @@ -8,6 +8,7 @@ import subprocess import sys import tempfile +from signal_run import run_without_core_dump from test_linux_load_alignment import PT_SCE_VERSION PT_NULL = 0 @@ -144,10 +145,10 @@ def main(): if sys.platform.startswith("linux") and platform.machine() in ("x86_64", "AMD64"): fixed_path.chmod(0o755) - executed = subprocess.run([str(fixed_path)], capture_output=True, timeout=20) + executed = run_without_core_dump([str(fixed_path)], capture_output=True, timeout=20) assert executed.returncode == -signal.SIGTRAP, ("the image did not run at the base with its relocation applied", executed.returncode) relocated_fixed_path.chmod(0o755) - executed = subprocess.run([str(relocated_fixed_path)], capture_output=True, timeout=20) + executed = run_without_core_dump([str(relocated_fixed_path)], capture_output=True, timeout=20) assert executed.returncode == -signal.SIGTRAP, ("the relocated-table image did not run at the base with its relocation applied", executed.returncode) for name, image, base, error in ( diff --git a/core/relinker/relinker/tests/test_linux_gnu_segments.py b/core/relinker/relinker/tests/test_linux_gnu_segments.py index bf83662654..223c131881 100644 --- a/core/relinker/relinker/tests/test_linux_gnu_segments.py +++ b/core/relinker/relinker/tests/test_linux_gnu_segments.py @@ -7,6 +7,7 @@ import subprocess import sys import tempfile +from signal_run import run_without_core_dump from test_linux_load_alignment import fixture PT_GNU_STACK = 0x6474E551 @@ -62,6 +63,17 @@ def main(): assert relro[3:5] == (0x500 + base, 0x500 + base), relro assert relro[5:7] == (0x100, 0x100), relro + dropped_stack = image() + struct.pack_into("\n" + "#include \n" + "static int replacement(void) { _exit(77); }\n" + "void* TraceImport_nid_no_patch(void* target, const char* name, const char* library) {\n" + " return strcmp(name, \"shared\") == 0 && strcmp(library, \"a.prx\") == 0 ? (void*)replacement : target;\n" + "}\n") +PLAIN = "int Unrelated(void) { return 0; }\n" + + +def relink(relinker, source, output, *options): + return subprocess.run([str(relinker), "--skip-sce-module", *options, str(source), str(output)], + capture_output=True, text=True, timeout=30) + + +def library(work, name, code): + source = work / (name + ".c") + source.write_text(code) + output = work / (name + ".so") + subprocess.run([os.environ.get("CC", "cc"), "-shared", "-fPIC", str(source), "-o", str(output)], check=True) + return output.read_bytes() + + +def main(): + relinker = Path(sys.argv[1]).resolve() + runnable = sys.platform.startswith("linux") and platform.machine() in ("x86_64", "AMD64") + with tempfile.TemporaryDirectory(prefix="anyps5-linux-trace-imports-") as directory: + work = Path(directory) + libraries = {} + if runnable: + libraries = {name: library(work, name, code) for name, code in (("a", PROVIDER), ("hook", HOOK), ("plain", PLAIN))} + + for name, options, libc, symbol_info, expected in ( + ("untraced", (), "hook", 0x12, 11), + ("traced", ("--trace-imports",), "hook", 0x12, 77), + ("fixed-base", ("--trace-imports", "--linux-fixed-base", "0x400000"), "hook", 0x12, 77), + ("data", ("--trace-imports",), "hook", 0x11, 11), + ("weak", ("--trace-imports",), "hook", 0x22, 11), + ("unsupported", ("--trace-imports",), "plain", 0x12, None)): + case = work / name + case.mkdir() + image = executable("a.prx", extra_dependencies=("libc.prx",)) + image[0x4898 + 4] = symbol_info + source = case / "input.elf" + source.write_bytes(image) + output = case / "output.elf" + result = relink(relinker, source, output, *options) + assert result.returncode == 0, (name, result.stdout, result.stderr) + if not runnable: + continue + (case / "libs").mkdir() + (case / "libs" / "a.prx").write_bytes(libraries["a"]) + (case / "libs" / "libc.prx").write_bytes(libraries[libc]) + output.chmod(0o755) + run = subprocess.run([str(output)], capture_output=True, text=True, timeout=30) + if expected is None: + assert run.returncode != 0 and "TraceImport_nid_no_patch" in run.stderr, (name, run.returncode, run.stdout, run.stderr) + else: + assert run.returncode == expected, (name, run.returncode, run.stdout, run.stderr) + + case = work / "without-libc" + case.mkdir() + source = case / "input.elf" + source.write_bytes(executable("a.prx")) + result = relink(relinker, source, case / "output.elf", "--trace-imports") + assert result.returncode == 2 and "--trace-imports requires libc.prx" in result.stderr, (result.returncode, result.stderr) + assert not (case / "output.elf").exists() + result = relink(relinker, source, case / "output.elf", "--trace-imports", "--lazy-binding") + assert result.returncode != 0 and "--trace-imports conflicts with --lazy-binding" in result.stderr, (result.returncode, result.stderr) + result = relink(relinker, source, case / "output", "--trace-imports", "--macos") + assert result.returncode != 0 and "--trace-imports cannot be used with --macos" in result.stderr, (result.returncode, result.stderr) + + print("Linux import tracing tests passed") + + +if __name__ == "__main__": + main() diff --git a/core/relinker/relinker/tests/test_macos_empty_tls.py b/core/relinker/relinker/tests/test_macos_empty_tls.py new file mode 100644 index 0000000000..7d4619ec23 --- /dev/null +++ b/core/relinker/relinker/tests/test_macos_empty_tls.py @@ -0,0 +1,70 @@ +"""Check `relinker --macos` on guests whose PT_TLS is empty.""" +from pathlib import Path +import struct +import subprocess +import sys +import tempfile + +from test_macos_output import MachO +from test_tls_function_coverage import make_image + + +def empty_tls_image(body): + image = make_image("register", "unwind", body=body + b"\xc3") + struct.pack_into("\n' + '#include \n' + '#include \n' + 'extern int FixtureExport(void);\n' + 'extern int FixtureData;\n' + 'void Aps5StartGuest_nid_no_patch(void) {\n' + ' int (*weak)(void) = dlsym(RTLD_DEFAULT, "FixtureWeak");\n' + ' int (*protected)(void) = dlsym(RTLD_DEFAULT, "FixtureProtected");\n' + ' if (!weak || weak() != 9 || !protected || protected() != 42) _exit(1);\n' + f' if ((uintptr_t)dlsym(RTLD_DEFAULT, "FixtureAbsolute") != {ABSOLUTE}ULL) _exit(2);\n' + ' if (dlsym(RTLD_DEFAULT, "FixtureHidden") || dlsym(RTLD_DEFAULT, "FixtureLocal")) _exit(3);\n' + ' _exit(FixtureExport() + FixtureData - 5);\n' + '}\n') + for name in ('libkernel.prx', 'libc.prx'): + subprocess.run(['xcrun', 'clang', '-arch', 'x86_64', '-dynamiclib', str(source), + '-undefined', 'dynamic_lookup', '-Wl,-install_name,@rpath/' + name, + '-o', str(libraries / name)], check=True, capture_output=True, timeout=20) + source.write_text('int FixtureHostLibc(void) { return 0; }\n') + output.chmod(0o755) + command = (['/usr/bin/arch', '-x86_64'] if platform.machine() == 'arm64' else []) + [str(output)] + run = subprocess.run(command, capture_output=True, timeout=10) + if run.returncode != 42 and b'Bad CPU type in executable' in run.stderr: + assert os.environ.get('GITHUB_ACTIONS') != 'true', 'macOS CI must execute the x86_64 export probe' + return + assert run.returncode == 42, (run.returncode, run.stdout, run.stderr) + print('macOS executable exports: native x86_64 imports and dlsym passed') + + +def main(): + relinker = Path(sys.argv[1]).resolve() + with tempfile.TemporaryDirectory(prefix='anyps5-macos-executable-exports-') as directory: + work = Path(directory) + run, output = convert(relinker, work, 'exports', executable()) + assert run.returncode == 0, (run.stdout, run.stderr) + data = output.read_bytes() + entries = exports(data) + base = MachO(data).segment('__ELF0')['vmaddr'] + assert entries == { + '_FixtureExport': (0, base + 0x210 - 0x140000000), + '_FixtureData': (0, base + 0x280 - 0x140000000), + '_FixtureWeak': (4, base + 0x220 - 0x140000000), + '_FixtureAbsolute': (2, ABSOLUTE), + '_FixtureProtected': (0, base + 0x210 - 0x140000000), + }, entries + assert MachO(data).flags & 0x8000 + native_execution(work, output) + tls_symbols = (('FixtureTls#A#B', 0x16, 0, 1, 4, 4),) + for info, flags in ((0x16, 0), (0x26, 4)): + run, output = convert(relinker, work, f'tls-{info}', executable((('FixtureTls#A#B', info, 0, 1, 4, 4),), tls=True)) + assert run.returncode == 0, (run.stdout, run.stderr) + data = output.read_bytes() + entry, = exports(data).values() + assert entry[0] == flags + assert bool(MachO(data).flags & 0x8000) == bool(flags) + meta = MachO(data).segment('__APS5DATA') + slot = meta['fileoff'] + entry[1] + 0x140000000 - meta['vmaddr'] + assert struct.unpack_from('> 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(" FUNCTION else FUNCTION + FUNCTION_SIZE - functions = [(EARLY_FUNCTION, FUNCTION_SIZE)] if early_function else [] - functions.append((FUNCTION, FUNCTION_SIZE)) + if functions is None: + functions = [(EARLY_FUNCTION, FUNCTION_SIZE)] if early_function else [] + functions.append((FUNCTION, FUNCTION_SIZE)) interpreter = FUNCTION - len(INTERPRETER) image = bytearray(0x1000) image[:16] = b"\x7fELF\x02\x01\x01" + bytes(9) - struct.pack_into(" 1; filtered=1" in result.stdout, (n_nops, result.stdout) + + entry_tail = bytearray(build(0)) + struct.pack_into(" 1; filtered=1" in result.stdout, result.stdout + print("Unused filter fallthrough padding tests passed") + + +if __name__ == "__main__": + main() diff --git a/core/relinker/relinker/tests/test_unused_filter_indirect.py b/core/relinker/relinker/tests/test_unused_filter_indirect.py index 7528f0434b..eda91aa3a0 100644 --- a/core/relinker/relinker/tests/test_unused_filter_indirect.py +++ b/core/relinker/relinker/tests/test_unused_filter_indirect.py @@ -17,16 +17,16 @@ def rel(next_address, target): return struct.pack(" && 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; @@ -63,7 +63,9 @@ inline constexpr std::uint32_t WrittenOverflowWord = sizeof(Fault) / sizeof(std: inline constexpr std::uint32_t WrittenCountWord = WrittenOverflowWord + 1; inline constexpr std::uint32_t WrittenSlotsWord = WrittenCountWord + 1; inline constexpr std::uint32_t WrittenListWord = WrittenSlotsWord + 2 * WrittenPageSlots; -inline constexpr std::size_t FaultBufferBytes = (WrittenListWord + WrittenPageSlots) * sizeof(std::uint32_t); +inline constexpr std::uint32_t SrtGuardWord = WrittenListWord + WrittenPageSlots; +inline constexpr std::uint32_t SrtGuardStageShift = 24; +inline constexpr std::size_t FaultBufferBytes = (SrtGuardWord + 1) * sizeof(std::uint32_t); static_assert((WrittenPageSlots & (WrittenPageSlots - 1)) == 0); static_assert(WrittenChunkPages == 32); diff --git a/core/shader/recompiler/CMakeLists.txt b/core/shader/recompiler/CMakeLists.txt index 3876230cdf..ad13332d35 100644 --- a/core/shader/recompiler/CMakeLists.txt +++ b/core/shader/recompiler/CMakeLists.txt @@ -112,6 +112,7 @@ function(add_shader_recompiler target runtimeTarget) ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/ShaderInfoCollector.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/SharedMemoryBarrierInserter.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/DenormalFlushEliminator.cpp + ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/VectorWindowStoreEliminator.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/RedundantValueEliminator.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/SrtWalker/SrtAddressArithmetic.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp @@ -168,6 +169,9 @@ function(add_shader_recompiler target runtimeTarget) ) target_compile_features(${target} PUBLIC cxx_std_20) + if(WIN32) + target_compile_options(${target} PRIVATE "SHELL:-include ${PROJECT_SOURCE_DIR}/core/libs/prx/libc/include/HostStandardStreams.hpp") + endif() if(ANYPS5_ENABLE_SPIRV_TOOLS) target_sources(${target} PRIVATE ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/SpirvBackend/src/SpirvOptimizer.cpp) @@ -236,6 +240,9 @@ function(add_shader_recompiler target runtimeTarget) DebugProbeActive DepthBitsTextureWidth HashBytes + LayoutBarycentricEmulation + MeshBarycentricLayoutOf + NativeCachePath_nid_no_patch ImageTableAbi::PoisonCode ImageTableAbi::PoisonReasonName ImageTableAbi::TableHeader diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index 65c06e673c..d95621f9a1 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -102,11 +102,27 @@ private: } static void appendMesh(std::vector& key, const RecompileRequest& request) { + if (request.shader.stage == ShaderStage::Fragment) { + append(key, request.graphics && request.graphics->mesh); + return; + } if (request.shader.stage != ShaderStage::Mesh) return; const auto* mesh = request.graphics && request.graphics->mesh ? &*request.graphics->mesh : nullptr; append(key, mesh != nullptr); if (mesh == nullptr) return; for (const auto value : {mesh->inputPrimitive, mesh->primitivesPerGroup, mesh->verticesPerGroup, mesh->maxVertices, mesh->maxPrimitives, mesh->threadsPerGroup, mesh->ldsSizeDwords, mesh->provokingVertex, mesh->esgsItemSize, mesh->outputPrimitiveSize}) append(key, value); + const auto& barycentrics = request.graphics->meshBarycentrics; + append(key, barycentrics.has_value()); + if (!barycentrics.has_value()) return; + append(key, barycentrics->emulation.active); + append(key, barycentrics->emulation.smooth); + append(key, barycentrics->emulation.linear); + append(key, barycentrics->parameters.size()); + for (const auto& parameter : barycentrics->parameters) { + append(key, parameter.location); + append(key, parameter.sourceLocation); + append(key, parameter.perVertex); + } } static void appendTessellation(std::vector& key, const RecompileRequest& request) { @@ -115,6 +131,7 @@ private: append(key, tessellation != nullptr); if (tessellation == nullptr) return; for (const auto value : {tessellation->inputControlPoints, tessellation->outputControlPoints, tessellation->domain, tessellation->partitioning, tessellation->outputTopology}) append(key, value); + if (request.shader.stage != ShaderStage::TessellationEvaluation) append(key, tessellation->mergedHull); if (request.shader.stage == ShaderStage::Local) return; append(key, tessellation->patchesPerGroup); append(key, tessellation->primitiveGroupSize); @@ -192,6 +209,7 @@ private: append(key, value.orderedPixelShader); append(key, value.alphaToCoverage); append(key, value.stencilRefExportEnable); + append(key, value.stencilOperationValueExport); append(key, value.ditheredAlphaToCoverage); append(key, value.quadPixelMask); append(key, value.quadSampleMask); @@ -199,6 +217,7 @@ private: append(key, value.positionLocation); append(key, value.sampleLocations); append(key, value.targetOutputMode); + append(key, value.sampleOrder); } static void append(std::vector& key, const ShaderVertexResourceDestination& value) { @@ -271,6 +290,8 @@ private: append(key, value.spannedWaves); append(key, value.robustBufferAccess2); append(key, value.float32FlushToZero); + append(key, value.waveLdsMemoryBarriers); + append(key, value.float32DenormPreserve); } }; diff --git a/core/shader/recompiler/ControlFlow/include/ControlFlow/ControlFlowGraph.hpp b/core/shader/recompiler/ControlFlow/include/ControlFlow/ControlFlowGraph.hpp index 3199579688..c9ee6e2fc7 100644 --- a/core/shader/recompiler/ControlFlow/include/ControlFlow/ControlFlowGraph.hpp +++ b/core/shader/recompiler/ControlFlow/include/ControlFlow/ControlFlowGraph.hpp @@ -108,6 +108,8 @@ struct ControlFlowGraph { std::vector codeTableLoadProgramCounters; std::vector codeTableLoads; std::vector unlinkedCallProgramCounters; + std::uint32_t linkEscapeWaveSize = 0; + std::string linkEscapeReason; std::uint32_t entryBlock = InvalidControlFlowId; bool hasFetchCall = false; std::uint32_t fetchCallProgramCounter = 0; diff --git a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp index fe2c3565a3..5a2459f5af 100644 --- a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp +++ b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -195,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]; @@ -223,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) || @@ -294,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; @@ -700,8 +708,13 @@ bool readsScalarDword(const RdnaInstruction& instruction, std::uint32_t reg, std return overlaps(instruction.source0, source0Count) || overlaps(instruction.source1, source1Count) || overlaps(instruction.source2, source2Count) || overlaps(instruction.source3, source3Count); } +struct LinkEscape { + std::uint32_t waveSize = 0; + std::string reason; +}; + template -void requireReturnAddressesStayInCalls(const RdnaProgram& program, const std::vector& calls, const std::vector& unlinked, std::uint32_t waveSize, const TError& escapeError) { +LinkEscape requireReturnAddressesStayInCalls(const RdnaProgram& program, const std::vector& calls, const std::vector& unlinked, std::uint32_t knownWaveSize, const TError& escapeError) { std::set links; for (const auto& call : calls) { if (!call.fetch) links.insert(call.linkRegister); @@ -730,45 +743,58 @@ void requireReturnAddressesStayInCalls(const RdnaProgram& program, const std::ve std::erase_if(next, [&](std::uint32_t successor) { return successor >= size; }); return next; }; - for (const auto link : links) { - const auto writesLink = [&](std::uint32_t index) { - const SwappcCall* call = findCallAt(calls, index); - return (call != nullptr && call->linkRegister == link) || (std::ranges::find(unlinked, index) != unlinked.end() && scalarIndex(program.instructions[index].destination) == link); - }; - std::vector holds(size, 0u); - std::vector work; - for (std::uint32_t index = 0; index < size; ++index) { - if (writesLink(index)) work.push_back(index); - } - const auto after = [&](std::uint32_t index) -> std::uint8_t { - if (writesLink(index)) return 3u; - std::uint8_t value = holds[index]; - if (overwritesScalar(program.instructions[index], link, waveSize)) value &= 2u; - if (overwritesScalar(program.instructions[index], link + 1u, waveSize)) value &= 1u; - return value; - }; - while (!work.empty()) { - const auto index = work.back(); - work.pop_back(); - const auto value = after(index); - for (const auto successor : successors(index)) { - if ((holds[successor] | value) == holds[successor]) continue; - holds[successor] |= value; - work.push_back(successor); + const auto escapeAt = [&](std::uint32_t waveSize) -> std::optional { + for (const auto link : links) { + const auto writesLink = [&](std::uint32_t index) { + const SwappcCall* call = findCallAt(calls, index); + return (call != nullptr && call->linkRegister == link) || (std::ranges::find(unlinked, index) != unlinked.end() && scalarIndex(program.instructions[index].destination) == link); + }; + std::vector holds(size, 0u); + std::vector work; + for (std::uint32_t index = 0; index < size; ++index) { + if (writesLink(index)) work.push_back(index); + } + const auto after = [&](std::uint32_t index) -> std::uint8_t { + if (writesLink(index)) return 3u; + std::uint8_t value = holds[index]; + if (overwritesScalar(program.instructions[index], link, waveSize)) value &= 2u; + if (overwritesScalar(program.instructions[index], link + 1u, waveSize)) value &= 1u; + return value; + }; + while (!work.empty()) { + const auto index = work.back(); + work.pop_back(); + const auto value = after(index); + for (const auto successor : successors(index)) { + if ((holds[successor] | value) == holds[successor]) continue; + holds[successor] |= value; + work.push_back(successor); + } + } + for (std::uint32_t index = 0; index < size; ++index) { + const auto& instruction = program.instructions[index]; + if (holds[index] == 0u || writesLink(index) || instruction.family == RdnaInstructionFamily::SOPC || findReturnAt(calls, index) != nullptr) continue; + const bool inside = std::ranges::any_of(calls, [&](const SwappcCall& call) { return !call.fetch && call.linkRegister == link && index >= call.targetIndex && index <= call.returnIndex; }); + if (inside || !(((holds[index] & 1u) != 0u && readsScalarDword(instruction, link, waveSize)) || ((holds[index] & 2u) != 0u && readsScalarDword(instruction, link + 1u, waveSize)))) continue; + const auto owner = std::ranges::find_if(calls, [&](const SwappcCall& call) { return !call.fetch && call.linkRegister == link; }); + return escapeError(*owner, instruction); } } - for (std::uint32_t index = 0; index < size; ++index) { - const auto& instruction = program.instructions[index]; - if (holds[index] == 0u || writesLink(index) || instruction.family == RdnaInstructionFamily::SOPC || findReturnAt(calls, index) != nullptr) continue; - const bool inside = std::ranges::any_of(calls, [&](const SwappcCall& call) { return !call.fetch && call.linkRegister == link && index >= call.targetIndex && index <= call.returnIndex; }); - if (inside || !(((holds[index] & 1u) != 0u && readsScalarDword(instruction, link, waveSize)) || ((holds[index] & 2u) != 0u && readsScalarDword(instruction, link + 1u, waveSize)))) continue; - const auto owner = std::ranges::find_if(calls, [&](const SwappcCall& call) { return !call.fetch && call.linkRegister == link; }); - throw escapeError(*owner, instruction); - } + return std::nullopt; + }; + if (knownWaveSize != 0u) { + if (const auto escape = escapeAt(knownWaveSize)) throw *escape; + return {}; } + const auto narrow = escapeAt(32u); + const auto wide = escapeAt(64u); + if (narrow && wide) throw *narrow; + if (narrow) return {32u, narrow->what()}; + if (wide) return {64u, wide->what()}; + return {}; } -std::vector analyzeSwappcCalls(const RdnaProgram& program, const SwappcInfo* swappc, std::vector& unlinked) { +std::vector analyzeSwappcCalls(const RdnaProgram& program, const SwappcInfo* swappc, std::vector& unlinked, LinkEscape& linkEscape) { std::vector calls; for (std::uint32_t index = 0; index < program.instructions.size(); ++index) { const auto& instruction = program.instructions[index]; @@ -911,7 +937,7 @@ std::vector analyzeSwappcCalls(const RdnaProgram& program, const Swa } } - requireReturnAddressesStayInCalls(program, calls, unlinked, swappc != nullptr ? swappc->waveSize : 0u, escapeError); + linkEscape = requireReturnAddressesStayInCalls(program, calls, unlinked, swappc != nullptr ? swappc->waveSize : 0u, escapeError); for (std::size_t first = 0; first < calls.size(); ++first) { if (!calls[first].fetch && calls[first].callIndex >= calls[first].targetIndex && calls[first].callIndex <= calls[first].returnIndex) { @@ -1310,6 +1336,49 @@ bool loadedDword(const RdnaProgram& program, std::uint32_t reg, std::uint32_t be return false; } +bool reachingWriter(const RdnaProgram& program, std::uint32_t reader, std::initializer_list indices, std::uint32_t& writer, std::uint32_t waveSize) { + std::multimap branches; + std::set callTargets; + for (std::uint32_t index = 0; index < program.instructions.size(); ++index) { + const auto& instruction = program.instructions[index]; + std::uint32_t target = 0; + if (IsDirectBranchOpcode(instruction.op)) branches.emplace(instruction.branchTarget, index); + else if (instruction.op == RdnaOpcode::SSetpcB64 && resolveSetpcTarget(program, index, target)) branches.emplace(target, index); + else if (instruction.op == RdnaOpcode::SCallB64) callTargets.insert(instruction.branchTarget); + } + std::vector seen(program.instructions.size()); + std::vector pending; + const auto predecessors = [&](std::uint32_t index) { + if (index == 0u || callTargets.contains(program.instructions[index].programCounter)) return false; + const auto before = pending.size(); + const auto op = program.instructions[index - 1u].op; + if (op != RdnaOpcode::SBranch && op != RdnaOpcode::SEndpgm && op != RdnaOpcode::SCodeEnd && op != RdnaOpcode::SSetpcB64) pending.push_back(index - 1u); + const auto [first, last] = branches.equal_range(program.instructions[index].programCounter); + for (auto branch = first; branch != last; ++branch) pending.push_back(branch->second); + return pending.size() != before; + }; + if (!predecessors(reader)) return false; + std::optional found; + while (!pending.empty()) { + const auto index = pending.back(); + pending.pop_back(); + if (seen[index]) continue; + seen[index] = true; + const auto& instruction = program.instructions[index]; + std::uint32_t target = 0; + if (instruction.op == RdnaOpcode::SCallB64 || (instruction.op == RdnaOpcode::SSwappcB64 && resolveSetpcTarget(program, index, target))) return false; + if (std::ranges::any_of(indices, [&](std::uint32_t reg) { return writesScalar(instruction, reg, waveSize); })) { + if (found.has_value() && *found != index) return false; + found = index; + continue; + } + if (!predecessors(index)) return false; + } + if (!found.has_value()) return false; + writer = *found; + return true; +} + std::uint32_t branchWord(std::uint32_t opcode, std::size_t from, std::size_t to, const char* what) { const auto offset = static_cast(to) - static_cast(from) - 1; if (offset < -0x8000 || offset > 0x7fff) throw std::invalid_argument(std::string(what) + " lands out of the 16-bit branch range"); @@ -1453,7 +1522,8 @@ std::vector FindTableCalls(const RdnaProgram& program, const SwappcIn const auto descriptor = scalarIndex(load.source0); if (load.source0.kind != RdnaOperandKind::ScalarRegister || descriptor % 4u != 0u || load.memoryOffset >= 0x100000u || (load.memoryOffset & 3u) != 0u) continue; std::uint32_t descriptorLoad = 0; - if (!findLastWriter(program, lowLoad, {descriptor, descriptor + 1u, descriptor + 2u, descriptor + 3u}, descriptorLoad, swappc.waveSize, reader) || !sameBlock(descriptorLoad, index)) continue; + const bool sameBlockWriter = findLastWriter(program, lowLoad, {descriptor, descriptor + 1u, descriptor + 2u, descriptor + 3u}, descriptorLoad, swappc.waveSize, reader) && sameBlock(descriptorLoad, index); + if (!sameBlockWriter && !reachingWriter(program, lowLoad, {descriptor, descriptor + 1u, descriptor + 2u, descriptor + 3u}, descriptorLoad, swappc.waveSize)) continue; const auto& table = program.instructions[descriptorLoad]; const auto pointer = scalarIndex(table.source0); if (table.op != RdnaOpcode::SLoadDwordx4 || !isScalar(table.destination, descriptor) || pointer == NoScalarRegister || pointer % 2u != 0u || table.source1.kind != RdnaOperandKind::Null || @@ -1559,9 +1629,12 @@ LinkedCalls LinkCalls(const RdnaProgram& program, std::span ControlFlowGraph GraphBuilder::Build(const RdnaProgram& program, const SwappcInfo* swappc) const { std::vector unlinked; - const std::vector calls = analyzeSwappcCalls(program, swappc, unlinked); + LinkEscape linkEscape; + const std::vector calls = analyzeSwappcCalls(program, swappc, unlinked, linkEscape); ControlFlowGraph graph; for (const auto index : unlinked) graph.unlinkedCallProgramCounters.push_back(program.instructions[index].programCounter); + graph.linkEscapeWaveSize = linkEscape.waveSize; + graph.linkEscapeReason = std::move(linkEscape.reason); graph.blocks = splitIntoBlocks(program, calls); linkBlocks(graph.blocks, program, calls); for (const auto& call : calls) { diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index f820226a91..11a9d92195 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -354,6 +354,8 @@ void writePixelInfo(Writer& writer, const ShaderPixelStageInfo& info) { writer.WriteBool(info.iteratesSamples); writer.WriteU8(static_cast(info.positionLocation)); for (const std::uint8_t location : info.sampleLocations) writer.WriteU8(location); + writer.WriteBool(info.stencilOperationValueExport); + for (const std::uint32_t order : info.sampleOrder) writer.WriteU32(order); } ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { @@ -421,6 +423,10 @@ ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { info.positionLocation = readPixelPositionLocation(reader); for (std::uint8_t& location : info.sampleLocations) location = reader.ReadU8(); } + if (version >= 37u) info.stencilOperationValueExport = reader.ReadBool(); + if (version >= 42u) { + for (std::uint32_t& order : info.sampleOrder) order = reader.ReadU32(); + } if (version < 5u) { const auto input = [](PixelInput value, bool present) { return present ? PixelInputBit(value) : 0u; }; info.inputAddr = input(PixelInput::PerspectiveSample, info.hasPerspectiveCenterVgpr && inputAddrOrCenterVgpr == 2u) | input(PixelInput::PerspectiveCenter, info.hasPerspectiveCenterVgpr) | @@ -561,6 +567,7 @@ void writeTessellationConfiguration(Writer& writer, const TessellationConfigurat writer.WriteU32(configuration.patchesPerGroup); writer.WriteU32(configuration.primitiveGroupSize); writer.WriteBool(configuration.breakAtInstance); + writer.WriteBool(configuration.mergedHull); } TessellationConfiguration readTessellationConfiguration(Reader& reader, std::uint32_t version) { @@ -575,6 +582,7 @@ TessellationConfiguration readTessellationConfiguration(Reader& reader, std::uin configuration.primitiveGroupSize = reader.ReadU32(); configuration.breakAtInstance = reader.ReadBool(); } + if (version >= 39u) configuration.mergedHull = reader.ReadBool(); return configuration; } @@ -747,6 +755,19 @@ void writeGraphicsCompileContext(Writer& writer, const GraphicsCompileContext& g writer.WriteU32(graphics.draw.indexCount); writer.WriteU32(graphics.draw.indexElementBytes); writer.WriteU32(graphics.draw.instanceCount); + writer.WriteBool(graphics.meshBarycentrics.has_value()); + if (graphics.meshBarycentrics.has_value()) { + const auto& barycentrics = *graphics.meshBarycentrics; + writer.WriteBool(barycentrics.emulation.active); + writer.WriteBool(barycentrics.emulation.smooth); + writer.WriteBool(barycentrics.emulation.linear); + writer.WriteU64(barycentrics.parameters.size()); + for (const auto& parameter : barycentrics.parameters) { + writer.WriteU32(parameter.location); + writer.WriteU32(parameter.sourceLocation); + writer.WriteBool(parameter.perVertex); + } + } } GraphicsCompileContext readGraphicsCompileContext(Reader& reader, DeserializedGraphicsCompileContext& storage, std::uint32_t version) { @@ -785,6 +806,22 @@ GraphicsCompileContext readGraphicsCompileContext(Reader& reader, DeserializedGr storage.draw.indexElementBytes = reader.ReadU32(); storage.draw.instanceCount = reader.ReadU32(); graphics.draw = storage.draw; + if (version >= 38u && reader.ReadBool()) { + MeshBarycentricLayout barycentrics; + barycentrics.emulation.active = reader.ReadBool(); + barycentrics.emulation.smooth = reader.ReadBool(); + barycentrics.emulation.linear = reader.ReadBool(); + const auto count = reader.ReadU64(); + reader.Require(count, 9u); + for (std::uint64_t i = 0; i < count; i++) { + FragmentParameter parameter{}; + parameter.location = reader.ReadU32(); + parameter.sourceLocation = reader.ReadU32(); + parameter.perVertex = reader.ReadBool(); + barycentrics.parameters.push_back(parameter); + } + graphics.meshBarycentrics = std::move(barycentrics); + } return graphics; } @@ -794,7 +831,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const std::string buffer; Writer writer(buffer); writer.WriteU32(0x41505335u); - writer.WriteU32(36u); + writer.WriteU32(42u); writeShaderBinary(writer, request.shader); writeGuestContext(writer, request.context); writeSpirvTarget(writer, request.target); @@ -825,6 +862,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const writer.WriteU32(request.context.vertex->depthRemapMode); } writer.WriteBool(request.target.float32FlushToZero); + writer.WriteBool(request.target.float32DenormPreserve); return base64Encode(buffer); } @@ -833,7 +871,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const Reader reader(decoded); if (reader.ReadU32() != 0x41505335u) throw std::runtime_error("invalid recompile request signature"); const auto version = reader.ReadU32(); - if (version < 1u || version > 36u) throw std::runtime_error("unsupported recompile request serialization version"); + if (version < 1u || version > 42u) throw std::runtime_error("unsupported recompile request serialization version"); DeserializedRequest result{}; result.request.shader = readShaderBinary(reader, result.shaderCode, result.shaderHeader); result.request.context = readGuestContext(reader, result, version); @@ -878,6 +916,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const } } if (version >= 36u) result.request.target.float32FlushToZero = reader.ReadBool(); + if (version >= 41u) result.request.target.float32DenormPreserve = reader.ReadBool(); if (version >= 21u && reader.Remaining() != 0u) throw std::runtime_error("RequestSerializer: unexpected trailing data"); return result; } diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp index cfc57354b8..32f615dfcb 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp @@ -88,6 +88,7 @@ struct ResourceBlock { std::vector successors; std::vector sources; std::vector images; + std::vector flatSlots; }; } diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ProgramMetadata.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ProgramMetadata.hpp index 1ed7da10a3..ae85d35ec0 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ProgramMetadata.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ProgramMetadata.hpp @@ -33,6 +33,7 @@ struct IrProgramMetadata { bool vectorWindowIndexedWrites = false; std::uint32_t tessellationControlStride = 0; std::uint32_t tessellationPatchStride = 0; + std::uint32_t tessellationSharedStride = 0; }; } diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp index 5fe5a3aa87..307b83cdff 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp @@ -93,10 +93,12 @@ struct ImageResource { bool cubeFaceCompatible = true; bool flatVolumeCompatible = true; bool singleMipPlaneBiasCompatible = false; + bool volumeArraySampleCompatible = false; bool flatLineCompatible = true; bool flatVolumeLineCompatible = true; bool foldTexelOffsetsOnly = false; bool convertedSamples = false; + bool remappedDepth1D = false; std::uint32_t byElements = 0; std::uint32_t byComponents = 0; IrBufferFormat packedFormat = IrBufferFormat::Invalid; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp index 83690a5aaf..bfa9785b83 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp @@ -16,7 +16,8 @@ enum class TessellationAttribute { Factor, PatchInput, InstanceOutput, - InstanceInput + InstanceInput, + ControlShared }; enum class TessellationBaseKind { diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp index 872aeae99e..c1ed5e72ed 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp @@ -433,6 +433,7 @@ enum class IrOpcode : std::uint16_t { DataAppend, DataConsume, DataOrderedCount, + DataOrderedSwap, SwizzleU32, ImageQueryDimensions, ImageQueryLod, diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrValue.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrValue.hpp index d7adec321f..ccdda16ed9 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrValue.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrValue.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -35,7 +36,7 @@ public: [[nodiscard]] IrType Type() const; [[nodiscard]] std::uint32_t Id() const; [[nodiscard]] const std::vector& Arguments() const; - [[nodiscard]] const std::vector& Uses() const; + [[nodiscard]] auto Uses() const; [[nodiscard]] IrBlock* Parent() const; [[nodiscard]] bool HasImmediate() const; [[nodiscard]] std::uint32_t ImmediateU32() const; @@ -103,10 +104,13 @@ private: std::vector operandUses; bool hasImmediate; std::vector arguments; - std::vector uses; IrBlock* parent; }; +inline auto IrValue::Uses() const { + return std::views::transform(operandUses, [](const IrUse& use) { return use.user; }); +} + inline IrOpcode IrValue::Opcode() const { return opcode; } diff --git a/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp b/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp index 650a852f61..b3622f4c6a 100644 --- a/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp @@ -474,6 +474,7 @@ constexpr std::array(IrOpcode::Count)> Meta makeMeta("DataAppend", U32, U32, U1, U32, U32), makeMeta("DataConsume", U32, U32, U1, U32, U32), makeMeta("DataOrderedCount", U32, U32, U1, U32, U32, U32), + makeMeta("DataOrderedSwap", U32, U32, U1, U32, U32, U32), makeMeta("SwizzleU32", U32, U32, U32, U1), makeMeta("ImageQueryDimensions", U32x4, ImageResource, ImageAddress, U1), makeMeta("ImageQueryLod", U32x4, ImageResource, SamplerResource, ImageAddress, U1), @@ -698,6 +699,7 @@ SharedAccess SharedAccessOf(IrOpcode opcode) { return SharedAccess::Atomic; case IrOpcode::DataAppend: case IrOpcode::DataOrderedCount: + case IrOpcode::DataOrderedSwap: return SharedAccess::Append; case IrOpcode::DataConsume: return SharedAccess::Consume; diff --git a/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp b/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp index 3ea52ae5b0..08dba6b1d9 100644 --- a/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp @@ -208,9 +208,22 @@ std::string valueToString(const std::map& ids, cons } std::size_t IrProgram::ReclaimUnusedValues() { - std::unordered_set live; + std::vector owned(nextValueId, nullptr); + for (const auto& value : values) owned[value->Id()] = value.get(); + std::vector live(nextValueId, false); + std::unordered_set liveElsewhere; std::vector pending; - const auto keep = [&](IrValue* value) { if (value != nullptr && live.insert(value).second) pending.push_back(value); }; + const auto keep = [&](IrValue* value) { + if (value == nullptr) return; + if (const auto id = value->Id(); id < owned.size() && owned[id] == value) { + if (live[id]) return; + live[id] = true; + } else if (!liveElsewhere.insert(value).second) { + return; + } + pending.push_back(value); + }; + const auto isLive = [&](const IrValue& value) { return live[value.Id()]; }; const auto keepBlocks = [&](const auto& storage) { for (const auto& block : storage) { for (IrValue* instruction : block->Instructions()) keep(instruction); @@ -235,13 +248,13 @@ std::size_t IrProgram::ReclaimUnusedValues() { for (std::size_t index = 0; index < value->ArgumentCount(); ++index) keep(value->Argument(index)); } for (const auto& value : values) { - if (live.contains(value.get())) continue; + if (isLive(*value)) continue; for (std::size_t index = 0; index < value->ArgumentCount(); ++index) value->ReplaceArgument(index, nullptr); } for (const auto& value : values) { - if (!live.contains(value.get()) && value->HasUses()) throw std::runtime_error("an unused IR value has an external user"); + if (!isLive(*value) && value->HasUses()) throw std::runtime_error("an unused IR value has an external user"); } - const auto removed = std::erase_if(values, [&](const auto& value) { return !live.contains(value.get()); }); + const auto removed = std::erase_if(values, [&](const auto& value) { return !isLive(*value); }); values.shrink_to_fit(); return removed; } diff --git a/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp b/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp index f5653e80f9..a7cc1db50d 100644 --- a/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp @@ -6,6 +6,7 @@ namespace ShaderRecompiler { IrValue::IrValue(IrOpcode opcode, IrType type, std::uint32_t id) : opcode(opcode), type(type), id(id), hasImmediate(false), parent(nullptr) { + arguments.reserve(IrOpcodeOperandCount(opcode)); } std::uint32_t IrValue::Id() const { @@ -16,10 +17,6 @@ const std::vector& IrValue::Arguments() const { return arguments; } -const std::vector& IrValue::Uses() const { - return uses; -} - IrBlock* IrValue::Parent() const { return parent; } @@ -33,7 +30,6 @@ void IrValue::AddArgument(IrValue* argument) { if (argument == nullptr) { return; } - argument->uses.push_back(this); argument->operandUses.push_back(IrUse{this, arguments.size() - 1}); } @@ -43,10 +39,6 @@ void IrValue::ReplaceArgument(std::size_t index, IrValue* argument) { } IrValue* previous = arguments[index]; if (previous != nullptr) { - const auto useIt = std::find(previous->uses.begin(), previous->uses.end(), this); - if (useIt != previous->uses.end()) { - previous->uses.erase(useIt); - } const auto operandIt = std::find_if(previous->operandUses.begin(), previous->operandUses.end(), [this, index](const IrUse& use) { return use.user == this && use.operand == index; }); @@ -56,7 +48,6 @@ void IrValue::ReplaceArgument(std::size_t index, IrValue* argument) { } arguments[index] = argument; if (argument != nullptr) { - argument->uses.push_back(this); argument->operandUses.push_back(IrUse{this, index}); } } @@ -77,17 +68,14 @@ void IrValue::ReplaceAllUsesWith(IrValue* replacement) { list.reserve(std::max(required, grown)); } }; - reserveAdditional(replacement->uses); reserveAdditional(replacement->operandUses); } for (auto use = operandUses.rbegin(); use != operandUses.rend(); ++use) { use->user->arguments[use->operand] = replacement; if (replacement != nullptr) { - replacement->uses.push_back(use->user); replacement->operandUses.push_back(*use); } } - uses.clear(); operandUses.clear(); } @@ -113,11 +101,11 @@ bool IrValue::MayHaveSideEffects() const { } bool IrValue::HasUses() const { - return !uses.empty(); + return !operandUses.empty(); } std::size_t IrValue::UseCount() const { - return uses.size(); + return operandUses.size(); } std::size_t IrValue::PhiBlockCount() const { @@ -224,7 +212,7 @@ void IrValue::ReplaceOpcode(IrOpcode opcode) { } void IrValue::Invalidate() { - if (!uses.empty()) { + if (!operandUses.empty()) { throw std::runtime_error("IrValue::Invalidate called on a value that still has uses"); } for (std::size_t index = 0; index < arguments.size(); index++) { diff --git a/core/shader/recompiler/IntermediateRepresentation/tests/UseRewrite.cpp b/core/shader/recompiler/IntermediateRepresentation/tests/UseRewrite.cpp index 3308434316..0849790845 100644 --- a/core/shader/recompiler/IntermediateRepresentation/tests/UseRewrite.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/tests/UseRewrite.cpp @@ -1,4 +1,5 @@ #include "IntermediateRepresentation/IrBlock.hpp" +#include #include #include #include @@ -92,7 +93,7 @@ void CheckSimpleReplacement() { Require(second->Argument(0) == target); const std::vector expected{{existing, 0}, {first, 2}, {first, 1}, {second, 0}, {first, 0}}; Require(target->OperandUses() == expected); - Require(target->Uses() == std::vector({existing, first, first, second, first})); + Require(std::ranges::equal(target->Uses(), std::vector({existing, first, first, second, first}))); first->ReplaceArgument(1, source); Require(source->UseCount() == 1); source->ReplaceAllUsesWith(nullptr); @@ -130,7 +131,7 @@ void CheckPhiAndIdentity() { } void CheckAllocationFailure() { - for (std::ptrdiff_t failure = 0; failure < 2; ++failure) { + for (std::ptrdiff_t failure = 0; failure < 1; ++failure) { Graph graph(12); for (std::size_t index = 2; index < 12; ++index) { graph.Value(index)->AddArgument(graph.Value(0)); @@ -169,7 +170,7 @@ void CheckGeometricGrowth() { countAllocations = true; for (auto& source : sources) source->ReplaceAllUsesWith(graph.Value(1)); countAllocations = false; - Require(allocations <= 2 * (std::bit_width(count) + 1)); + Require(allocations <= std::bit_width(count) + 1); Require(graph.Value(1)->UseCount() == count); for (std::size_t index = 2; index < count + 2; ++index) Require(graph.Value(index)->Argument(0) == graph.Value(1)); } diff --git a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp index 64abe7b50e..84034d4306 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp @@ -123,6 +123,7 @@ struct ShaderTessellationInputInfo { bool breakAtInstance = false; std::uint32_t instancePatches = 0; bool drawParameters = false; + bool mergedHull = false; }; struct ShaderVertexInputInfo { @@ -181,6 +182,7 @@ struct ShaderPixelInputInfo { bool psDepthExportEnable = false; bool psSampleMaskExportEnable = false; bool psStencilRefExportEnable = false; + bool psStencilOperationValueExport = false; bool psSampleShading = false; bool psEarlyZ = false; bool psExecuteOnNoop = false; @@ -193,6 +195,7 @@ struct ShaderPixelInputInfo { bool psIteratesSamples = false; PixelPositionLocation psPositionLocation = PixelPositionLocation::Center; std::array sampleLocations{}; + std::array sampleOrder{}; ShaderStageRuntime stage; bool HasPositionInput() const { @@ -210,6 +213,10 @@ struct ShaderPixelInputInfo { return false; } + [[nodiscard]] bool ReordersSamples() const { + return std::ranges::any_of(sampleOrder, [](std::uint32_t order) { return order != 0u && order != 0x76543210u; }); + } + [[nodiscard]] bool QuadSampleMaskPartial() const { return std::ranges::any_of(quadSampleMask, [](std::uint16_t mask) { return mask != 0xffffu; }); } diff --git a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtInstructionPredicates.hpp b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtInstructionPredicates.hpp index 8a6b407e01..fa1310cff9 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtInstructionPredicates.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtInstructionPredicates.hpp @@ -3,6 +3,9 @@ #include "IntermediateRepresentation/IrProgram.hpp" +#include +#include + namespace ShaderRecompiler::Detail { bool IsRawRead(const IrResourcePlan& program, const IrValue& inst); @@ -12,6 +15,26 @@ bool IsDescriptorHandle(IrOpcode opcode); bool IsRuntimeSelect(IrOpcode opcode); bool IsRuntimeUniformOp(IrOpcode opcode); +bool ActiveMaskImplies(const IrValue* mask, const IrValue* condition); + +inline bool ActiveMaskImplies(IrValue* mask, IrValue* condition) { + return ActiveMaskImplies(static_cast(mask), static_cast(condition)); +} + +inline bool ActiveMaskImplies(const IrValue* mask, IrValue* condition) { + return ActiveMaskImplies(mask, static_cast(condition)); +} + +inline bool ActiveMaskImplies(IrValue* mask, const IrValue* condition) { + return ActiveMaskImplies(static_cast(mask), condition); +} + +template +requires (!std::is_pointer_v>) +bool ActiveMaskImplies(const TNode& mask, const TNode& condition) { + return ActiveMaskImplies(mask.Value(), condition.Value()); +} + } #endif diff --git a/core/shader/recompiler/Optimization/include/Optimization/TessellationLowering.hpp b/core/shader/recompiler/Optimization/include/Optimization/TessellationLowering.hpp index 9c524f45e0..7032578c3b 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/TessellationLowering.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/TessellationLowering.hpp @@ -3,12 +3,13 @@ #include "IntermediateRepresentation/IrProgram.hpp" #include "Optimization/ShaderStageInputInfo.hpp" +#include "Recompiler.hpp" namespace ShaderRecompiler { class TessellationLowering { public: - void Lower(IrProgram& program, const ShaderTessellationInputInfo& tessellation) const; + void Lower(IrProgram& program, const ShaderTessellationInputInfo& tessellation, const TessellationTargetLimits* limits = nullptr) const; }; } diff --git a/core/shader/recompiler/Optimization/include/Optimization/VectorWindowStoreEliminator.hpp b/core/shader/recompiler/Optimization/include/Optimization/VectorWindowStoreEliminator.hpp new file mode 100644 index 0000000000..fd8d5f6ac7 --- /dev/null +++ b/core/shader/recompiler/Optimization/include/Optimization/VectorWindowStoreEliminator.hpp @@ -0,0 +1,20 @@ +#ifndef CORE_SHADER_RECOMPILIER_OPTIMIZATION_INCLUDE_OPTIMIZATION_VECTORWINDOWSTOREELIMINATOR_HPP +#define CORE_SHADER_RECOMPILIER_OPTIMIZATION_INCLUDE_OPTIMIZATION_VECTORWINDOWSTOREELIMINATOR_HPP + +#include "IntermediateRepresentation/IrProgram.hpp" +#include + +namespace ShaderRecompiler { + +struct VectorWindowStoreEliminationStats { + std::uint32_t removedStores = 0; +}; + +class VectorWindowStoreEliminator { +public: + [[nodiscard]] VectorWindowStoreEliminationStats Eliminate(IrProgram& program) const; +}; + +} + +#endif diff --git a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp index b88053bd7e..1eb0dd3650 100644 --- a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp +++ b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp @@ -83,6 +83,152 @@ 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 feedsMultiply(const IrValue& value) { + const auto& uses = value.Uses(); + return std::any_of(uses.begin(), uses.end(), [](const IrValue* use) { return use->Opcode() == IrOpcode::IMul32; }); +} + bool isImmediateU32(const IrValue& value, std::uint32_t immediate) { return isImmediate(value, IrType::U32) && value.ImmediateU32() == immediate; } @@ -738,6 +884,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 +908,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 && !feedsMultiply(value)) { + 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 +934,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 +1072,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 +1089,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 +1140,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 +1161,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..170b97d6b8 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include #include #include #include @@ -356,6 +358,22 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l const auto first = PipelineSpecialization::ImageBase + index * PipelineSpecialization::ImageWords; plan.specialization.push_back({first, imageModes[index]}); for (std::uint32_t component = 0; component < 4u; ++component) plan.specialization.push_back({first + 1u + component, (snapshot.images.at(index).dwords[3] >> (component * 3u)) & 7u}); + const auto& image = info.images[index]; + if (image.resourceClass != ImageResourceClass::Storage || !image.written || image.atomic) continue; + const auto code = (snapshot.images.at(index).dwords[1] >> 20u) & 0x1ffu; + const auto format = code >= 1u && code <= 77u ? GetFormatInfo(static_cast(code)) : SpirvBufferFormatInfo{}; + const bool snorm = format.type == SpirvFormatComponentType::Snorm; + const auto bits = format.componentBits[0]; + const bool uniform = format.componentCount != 0u && !format.packedBitfield && bits >= 2u && bits <= 16u && std::all_of(format.componentBits, format.componentBits + format.componentCount, [&](std::uint32_t width) { return width == bits; }); + const bool quantized = uniform && (format.type == SpirvFormatComponentType::Unorm || snorm); + const auto scale = quantized ? static_cast((1u << (snorm ? bits - 1u : bits)) - 1u) : 1.0f; + auto reciprocal = 1.0f / scale; + while (static_cast(reciprocal) * static_cast(scale) < 1.0) reciprocal = std::nextafter(reciprocal, 2.0f); + const auto quantize = PipelineSpecialization::StoreQuantizeBase + index * PipelineSpecialization::StoreQuantizeWords; + plan.specialization.push_back({quantize, quantized ? 1u : 0u}); + plan.specialization.push_back({quantize + 1u, std::bit_cast(scale)}); + plan.specialization.push_back({quantize + 2u, std::bit_cast(reciprocal)}); + plan.specialization.push_back({quantize + 3u, quantized && snorm ? 1u : 0u}); } for (std::uint32_t target = 0; target < exportMappings.size() && !registrationModules; ++target) { for (std::uint32_t component = 0; component < 4u; ++component) plan.specialization.push_back({PipelineSpecialization::ExportBase + target * 4u + component, (exportMappings[target] >> (component * 2u)) & 3u}); @@ -364,7 +382,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/MaskedSelectEliminator.cpp b/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp index 7124e58f1b..a88fa3e288 100644 --- a/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp +++ b/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp @@ -1184,15 +1184,21 @@ private: bool solving = false; }; -std::uint32_t removeFullWaveSelects(IrProgram& program) { +std::uint32_t removeFullWaveSelects(IrProgram& program, const std::vector& regions, std::vector& selects) { std::uint32_t removed = 0; + selects.clear(); for (auto blockIt = program.BlockOrder().rbegin(); blockIt != program.BlockOrder().rend(); ++blockIt) { + const bool single = regionOf(regions, *blockIt) == Region::Once; auto& instructions = (*blockIt)->Instructions(); auto it = instructions.end(); while (it != instructions.begin()) { --it; IrValue* inst = *it; - if (!isSelect(inst->Opcode()) || inst->ArgumentCount() != 3u || !inst->HasUses() || !alwaysTrue(inst->Argument(0), program.WaveSize())) continue; + if (!isSelect(inst->Opcode()) || inst->ArgumentCount() != 3u) continue; + if (!inst->HasUses() || !alwaysTrue(inst->Argument(0), program.WaveSize())) { + if (single) selects.push_back(inst); + continue; + } inst->ReplaceAllUsesWith(inst->Argument(1)); inst->Invalidate(); inst->SetParent(nullptr); @@ -1203,17 +1209,22 @@ std::uint32_t removeFullWaveSelects(IrProgram& program) { return removed; } +bool hasUnreadCandidate(const std::vector& selects, const std::vector& regions, MaskFamily family, std::uint32_t waveSize) { + return std::any_of(selects.begin(), selects.end(), [&](const IrValue* select) { return select->Parent() != nullptr && select->HasUses() && familyOf(regions, select->Argument(0)->Resolve(), waveSize) == family; }); +} + } MaskedSelectEliminationStats MaskedSelectEliminator::Eliminate(IrProgram& program) const { MaskedSelectEliminationStats stats; const auto regions = blockRegions(program); const BlockSet uniform = uniformEntryBlocks(program); + std::vector selects; std::uint32_t removed = 1; while (removed != 0u) { - removed = removeFullWaveSelects(program); + removed = removeFullWaveSelects(program, regions, selects); for (const MaskFamily family : {MaskFamily{true, false}, MaskFamily{true, true}, MaskFamily{false, false}, MaskFamily{false, true}}) { - removed += UnreadLanes(program, regions, uniform, family, stats.steps).Remove(); + if (hasUnreadCandidate(selects, regions, family, program.WaveSize())) removed += UnreadLanes(program, regions, uniform, family, stats.steps).Remove(); } stats.removedSelects += removed; if (removed != 0u) DeadCodeEliminator{}.Eliminate(program); 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/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 5b869a4c9e..2ccd9a4039 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -192,6 +192,13 @@ ImageDecode inspectImageDescriptor(const DescriptorValue& descriptor, const Imag return rejectImage(invalidImageReason(descriptor), "image descriptor is invalid"); } decoded.dimension = descriptorDimension(descriptor, base.dimension); + if (base.depthCompare) { + if (decoded.dimension == RdnaImageDimension::Dim1D) { + decoded.dimension = RdnaImageDimension::Dim2D; + } else if (decoded.dimension == RdnaImageDimension::Dim1DArray) { + decoded.dimension = RdnaImageDimension::Dim2DArray; + } + } decoded.cube = descriptorIsCube(descriptor); const auto format = rawImageFormat(descriptor); if (base.atomic64 && format != IrBufferFormat::Format32_32UInt && format != IrBufferFormat::Format32_32SInt && format != IrBufferFormat::Format32_32Float) { @@ -1850,11 +1857,17 @@ std::vector ResourceMaterializer::RuntimeImageModes(const ImageRe const bool volumeLoad = (image.dimension == RdnaImageDimension::Dim2D || image.dimension == RdnaImageDimension::Dim2DArray) && image.fmaskCompatible; const bool volumeStore = image.dimension == RdnaImageDimension::Dim2DArray && storage && image.written && !image.read && !image.atomic && image.mipMode != ImageMipMode::DynamicStorage; const bool volumePlaneBias = image.dimension == RdnaImageDimension::Dim2D && image.singleMipPlaneBiasCompatible && !storage && image.table == NoTable && conversion == IrBufferFormat::Invalid; - if ((volumeLoad || volumeStore || volumePlaneBias) && !depth && packed == IrBufferFormat::Invalid && image.byElements == 0u) { + const bool volumeArraySample = image.dimension == RdnaImageDimension::Dim2DArray && image.volumeArraySampleCompatible && !storage && image.table == NoTable && conversion == IrBufferFormat::Invalid; + if ((volumeLoad || volumeStore || volumePlaneBias || volumeArraySample) && !depth && packed == IrBufferFormat::Invalid && image.byElements == 0u) { auto volume = mode; volume.dimension = RdnaImageDimension::Dim3D; modes.push_back(volume); } + if (image.dimension == RdnaImageDimension::Dim2DMsaa && image.fmaskCompatible && image.cubeFaceCompatible && !table && !storage && !depth && conversion == IrBufferFormat::Invalid && packed == IrBufferFormat::Invalid && image.byElements == 0u) { + auto plane = mode; + plane.dimension = RdnaImageDimension::Dim2D; + modes.push_back(plane); + } if (image.dimension == RdnaImageDimension::Dim3D && image.flatVolumeCompatible && !storage && !depth && conversion == IrBufferFormat::Invalid && packed == IrBufferFormat::Invalid) { for (const auto dimension : {RdnaImageDimension::Dim2D, RdnaImageDimension::Dim2DArray}) { auto plane = mode; @@ -1894,7 +1907,7 @@ std::vector ResourceMaterializer::RuntimeImageModes(const ImageRe plain.dimension = image.dimension == RdnaImageDimension::Dim1DArray ? RdnaImageDimension::Dim1D : image.dimension == RdnaImageDimension::Dim2DArray ? RdnaImageDimension::Dim2D : RdnaImageDimension::Dim2DMsaa; modes.push_back(plain); } - if (image.dimension == RdnaImageDimension::Dim2DArray) { + if (image.dimension == RdnaImageDimension::Dim2DArray && !image.remappedDepth1D) { mode.cube = true; modes.push_back(mode); } @@ -1950,7 +1963,7 @@ std::vector ResourceMaterializer::RuntimeImageModes(const ImageRe mode.packedFormat = IrBufferFormat::Invalid; mode.shaderSwizzle = ShaderImageIdentitySwizzle; modes.push_back(mode); - if (image.dimension == RdnaImageDimension::Dim2DArray) { + if (image.dimension == RdnaImageDimension::Dim2DArray && !image.remappedDepth1D) { auto plain = mode; plain.dimension = RdnaImageDimension::Dim2D; modes.push_back(plain); diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index 7306a87c0e..6f52b4489a 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -83,6 +83,10 @@ std::uint32_t possibleU32Bits(const IrValue* value) { const IrValue* shift = value->Argument(1)->Resolve(); return shift->HasImmediate() && shift->Type() == IrType::U32 ? possibleU32Bits(value->Argument(0)) << (shift->ImmediateU32() & 31u) : std::numeric_limits::max(); } + case IrOpcode::ReadFirstLane: + return value->ArgumentCount() >= 1u ? possibleU32Bits(value->Argument(0)) : std::numeric_limits::max(); + case IrOpcode::SelectU32: + return value->ArgumentCount() == 3u ? (possibleU32Bits(value->Argument(1)) | possibleU32Bits(value->Argument(2))) : std::numeric_limits::max(); default: return std::numeric_limits::max(); } @@ -322,6 +326,12 @@ private: } block.sources = m_blockSources[index]; block.images = m_blockImages[index]; + for (const IrValue* inst : order[index]->Instructions()) { + if (inst->Opcode() != IrOpcode::ReadConst || inst->ArgumentCount() != 2u || inst->Argument(0)->Resolve()->Opcode() != IrOpcode::GetSrtResource) continue; + const IrValue* slot = inst->Argument(1)->Resolve(); + if (!slot->HasImmediate() || slot->Type() != IrType::U32 || slot->ImmediateU32() >= m_program.Resources().srtReads.size()) fail("resource control flow has an invalid flat SRT slot"); + if (std::ranges::find(block.flatSlots, slot->ImmediateU32()) == block.flatSlots.end()) block.flatSlots.push_back(slot->ImmediateU32()); + } } std::vector unconditional(order.size()); for (std::uint32_t index = 0; index < order.size() && unconditional[index] == 0u;) { @@ -333,6 +343,7 @@ private: } std::vector reached(m_sources.size()); std::vector reachedImages(m_info.images.size()); + std::vector reachedFlat(m_program.Resources().srtReads.size()); for (std::uint32_t index = 0; index < order.size(); index++) { if (unconditional[index] == 0u) { continue; @@ -343,6 +354,9 @@ private: for (const auto image : controlFlow[index].images) { reachedImages[image] = 1u; } + for (const auto slot : controlFlow[index].flatSlots) { + reachedFlat[slot] = 1u; + } } bool guarded = false; for (const auto& block : controlFlow) { @@ -352,6 +366,9 @@ private: for (const auto image : block.images) { guarded = guarded || reachedImages[image] == 0u; } + for (const auto slot : block.flatSlots) { + guarded = guarded || reachedFlat[slot] == 0u; + } } if (guarded) { m_program.Resources().controlFlow = std::move(controlFlow); @@ -1746,9 +1763,12 @@ private: ImageResource image; image.source = source; image.resourceClass = ImageOpcodeInfoOf(op).resourceClass; - image.dimension = memory.imageDimension; - image.mipMode = image.resourceClass == ImageResourceClass::Storage && memory.imageHasMip ? ImageMipMode::DynamicStorage : ImageMipMode::None; image.depthCompare = (memory.imageSampleFlags & RdnaImageSampleFlagCompare) != 0; + image.remappedDepth1D = image.depthCompare && (memory.imageDimension == RdnaImageDimension::Dim1D || memory.imageDimension == RdnaImageDimension::Dim1DArray); + image.dimension = image.remappedDepth1D + ? (memory.imageDimension == RdnaImageDimension::Dim1D ? RdnaImageDimension::Dim2D : RdnaImageDimension::Dim2DArray) + : memory.imageDimension; + image.mipMode = image.resourceClass == ImageResourceClass::Storage && memory.imageHasMip ? ImageMipMode::DynamicStorage : ImageMipMode::None; image.r128 = memory.imageR128; image.packed = memory.imagePacked; image.byElements = memory.imageByElements; @@ -1757,7 +1777,7 @@ private: } static bool SameImageKey(const ImageResource& image, const ImageResource& key) { - return image.source == key.source && image.resourceClass == key.resourceClass && image.dimension == key.dimension && image.mipMode == key.mipMode && image.depthCompare == key.depthCompare && image.r128 == key.r128 && image.packed == key.packed && image.byElements == key.byElements && image.byComponents == key.byComponents; + return image.source == key.source && image.resourceClass == key.resourceClass && image.dimension == key.dimension && image.mipMode == key.mipMode && image.depthCompare == key.depthCompare && image.remappedDepth1D == key.remappedDepth1D && image.r128 == key.r128 && image.packed == key.packed && image.byElements == key.byElements && image.byComponents == key.byComponents; } std::uint32_t AddImage(std::uint32_t source, const MemoryInfo& memory, IrOpcode op, std::uint32_t pc) { @@ -1774,6 +1794,7 @@ private: } ImageResource image = key; image.singleMipPlaneBiasCompatible = m_program.Resources().stage == IrShaderStage::Pixel; + image.volumeArraySampleCompatible = true; image.firstUsePc = pc; Merge(image, memory, op, pc); m_info.images.push_back(image); @@ -1802,6 +1823,7 @@ private: const bool cubeFaceSample = op == IrOpcode::ImageSampleRaw && (memory.imageSampleFlags == 0u || memory.imageSampleFlags == RdnaImageSampleFlagLevelZero || memory.imageSampleFlags == RdnaImageSampleFlagLod); const bool cubeFaceStore = op == IrOpcode::ImageWrite && !memory.imageHasMip; image.cubeFaceCompatible = image.cubeFaceCompatible && (cubeFaceLoad || cubeFaceSample || cubeFaceStore) && !memory.imagePacked && memory.imageByElements == 0u; + image.volumeArraySampleCompatible = image.volumeArraySampleCompatible && cubeFaceSample && !memory.imageR128 && !memory.imagePacked && memory.imageByElements == 0u; const bool flatVolumeLoad = op == IrOpcode::ImageRead && memory.imageSampleFlags == 0u; const bool flatVolumeSample = op == IrOpcode::ImageSampleRaw && memory.imageSampleFlags == RdnaImageSampleFlagLevelZero; const bool flatVolumeQuery = op == IrOpcode::ImageQueryDimensions; diff --git a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp index 5a97337b76..135e1cd331 100644 --- a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp +++ b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp @@ -246,7 +246,7 @@ bool IsPixelParameterFlat(const ShaderPixelInputInfo& pixel, std::uint32_t input } void CollectPixelInputs(const IrProgram& program, const ShaderPixelInputInfo* pixel, ShaderInfo& info) { - if (pixel->HasPositionInput() || pixel->psDitheredAlphaToCoverage || pixel->QuadMasked() || pixel->SampleLocationsVary()) { + if (pixel->HasPositionInput() || pixel->psDitheredAlphaToCoverage || pixel->QuadMasked() || pixel->SampleLocationsVary() || pixel->ReordersSamples()) { AddInput(info, StageInputKind::FragCoord, 0, 4, "gl_FragCoord"); } if (pixel->psFrontFace) { @@ -336,10 +336,16 @@ void CollectComputeInputs(const ShaderComputeInputInfo* compute, ShaderInfo& inf } } +bool OwnControlPointWrite(const IrValue& inst) { + if (inst.Opcode() != IrOpcode::SetTessellationAttribute) return false; + const auto kind = static_cast(inst.Argument(0)->ImmediateU32()); + return kind == TessellationAttribute::ControlOutput || kind == TessellationAttribute::ControlShared; +} + void CollectBuiltinInputs(const IrProgram& program, ShaderInfo& info) { for (const auto& block : program.Blocks()) { for (const IrValue* inst : block->Instructions()) { - if (program.Resources().stage == IrShaderStage::TessellationControl && inst->Opcode() == IrOpcode::LaneId) { + if (program.Resources().stage == IrShaderStage::TessellationControl && (inst->Opcode() == IrOpcode::LaneId || OwnControlPointWrite(*inst))) { AddInput(info, StageInputKind::InvocationId, 0, 1, "gl_InvocationID"); } if (inst->Opcode() != IrOpcode::GetBuiltin) { diff --git a/core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp index 490238e349..e216d7b3a1 100644 --- a/core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp +++ b/core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp @@ -65,8 +65,10 @@ bool evaluateRuntimeSources(const IrResourcePlan& program, std::span cleanEvaluator(program, cleanRuntime); BasicEvaluator evaluator(program, runtime, cleanFlatSlots, &cleanEvaluator); std::vector active; + std::vector activeFlat; if (evaluateFlat) { active.assign(program.descriptorSources.size() + program.info.images.size(), 1u); + activeFlat.assign(program.srtReads.size(), 1u); } if (evaluateFlat && !program.controlFlow.empty()) { for (const auto& block : program.controlFlow) { @@ -76,6 +78,7 @@ bool evaluateRuntimeSources(const IrResourcePlan& program, std::span visited(program.controlFlow.size()); std::vector pending {0}; @@ -97,6 +100,7 @@ bool evaluateRuntimeSources(const IrResourcePlan& program, std::span::EvaluateWide(TNode value, std::uint64_t& result) { return false; } const TNode& inst = value; - if (_masked && IsRuntimeSelect(inst.Opcode()) && inst.ArgumentCount() == 3 && inst.Argument(0) == _activeMask) { + if (_masked && IsRuntimeSelect(inst.Opcode()) && inst.ArgumentCount() == 3 && ActiveMaskImplies(_activeMask, inst.Argument(0))) { return EvaluateWide(inst.Argument(1), result); } if (inst.Id() < _dense.Size()) { @@ -273,6 +273,9 @@ std::optional evaluatedArguments(IrOpcode opcode) { case IrOpcode::FPTrunc32: case IrOpcode::FPIsNan32: case IrOpcode::BitwiseNot32: + case IrOpcode::BitCount32: + case IrOpcode::FindILsb32: + case IrOpcode::FindUMsb32: case IrOpcode::LogicalNot: return 1u; case IrOpcode::CompositeConstructU64: case IrOpcode::IAdd32: @@ -368,7 +371,7 @@ template bool BasicEvaluator::MayEvaluate(const IrResourcePlan& program, IrValue* value, const IrValue* activeMask, std::unordered_set& firstLanes) { std::unordered_map known; const auto takesActiveArm = [activeMask](const IrValue* current) { - return activeMask != nullptr && IsRuntimeSelect(current->Opcode()) && current->ArgumentCount() == 3 && current->Argument(0)->Resolve() == activeMask; + return activeMask != nullptr && IsRuntimeSelect(current->Opcode()) && current->ArgumentCount() == 3 && ActiveMaskImplies(activeMask, current->Argument(0)); }; const auto requirements = [&program, &takesActiveArm](IrValue* current) { std::vector required; @@ -626,6 +629,26 @@ bool BasicEvaluator::EvaluateInst(const TNode& inst, std::uint64_t& resul return true; } return false; + case IrOpcode::BitCount32: + if (Arg(inst, 0, a)) { + result = static_cast(std::popcount(static_cast(a))); + return true; + } + return false; + case IrOpcode::FindILsb32: + if (Arg(inst, 0, a)) { + const auto val = static_cast(a); + result = val == 0u ? 0xffffffffu : static_cast(std::countr_zero(val)); + return true; + } + return false; + case IrOpcode::FindUMsb32: + if (Arg(inst, 0, a)) { + const auto val = static_cast(a); + result = val == 0u ? 0xffffffffu : static_cast(31u - std::countl_zero(val)); + return true; + } + return false; case IrOpcode::ShiftLeftLogical32: if (binary()) { result = static_cast(a) << (b & 31u); diff --git a/core/shader/recompiler/Optimization/src/SrtWalker/SrtInstructionPredicates.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtInstructionPredicates.cpp index 224d3b1c45..855fd960af 100644 --- a/core/shader/recompiler/Optimization/src/SrtWalker/SrtInstructionPredicates.cpp +++ b/core/shader/recompiler/Optimization/src/SrtWalker/SrtInstructionPredicates.cpp @@ -52,6 +52,17 @@ bool IsRuntimeSelect(IrOpcode opcode) { return opcode == IrOpcode::SelectU1 || opcode == IrOpcode::SelectU32 || opcode == IrOpcode::SelectF32; } +bool ActiveMaskImplies(const IrValue* mask, const IrValue* condition) { + if (mask == nullptr || condition == nullptr) return false; + mask = mask->Resolve(); + condition = condition->Resolve(); + if (mask == condition) return true; + if (mask->Opcode() == IrOpcode::LogicalAnd && mask->ArgumentCount() == 2) { + return ActiveMaskImplies(mask->Argument(0), condition) || ActiveMaskImplies(mask->Argument(1), condition); + } + return false; +} + bool IsRuntimeUniformOp(IrOpcode opcode) { switch (opcode) { case IrOpcode::BitCastU32F32: @@ -85,6 +96,9 @@ bool IsRuntimeUniformOp(IrOpcode opcode) { case IrOpcode::BitwiseOr32: case IrOpcode::BitwiseXor32: case IrOpcode::BitwiseNot32: + case IrOpcode::BitCount32: + case IrOpcode::FindILsb32: + case IrOpcode::FindUMsb32: case IrOpcode::SelectU1: case IrOpcode::SelectU32: case IrOpcode::SelectF32: diff --git a/core/shader/recompiler/Optimization/src/SrtWalker/SrtPlanBuilder.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtPlanBuilder.cpp index 6a1a86eae6..5cae7e7cf5 100644 --- a/core/shader/recompiler/Optimization/src/SrtWalker/SrtPlanBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/SrtWalker/SrtPlanBuilder.cpp @@ -117,12 +117,12 @@ void PlanBuilder::PatchReads() { throw std::runtime_error("SrtWalker::BuildPlan patched value has no parent block"); } builder.SetInsertionPoint(*block); - IrValue& resource = builder.Emit(IrOpcode::GetSrtResource, IrType::SrtResource, {}); - block->RemoveInstruction(&resource); + IrValue& resource = _program.CreateValue(IrOpcode::GetSrtResource, IrType::SrtResource); block->InsertInstructionBefore(patch.inst, &resource); IrValue& slotConstant = builder.Constant(patch.slot); - IrValue& flat = builder.Emit(IrOpcode::ReadConst, IrType::U32, {&resource, &slotConstant}); - block->RemoveInstruction(&flat); + IrValue& flat = _program.CreateValue(IrOpcode::ReadConst, IrType::U32); + flat.AddArgument(&resource); + flat.AddArgument(&slotConstant); block->InsertInstructionBefore(patch.inst, &flat); patch.inst->ReplaceAllUsesWith(&flat); for (auto& info : _program.Metadata().blockInfo) { diff --git a/core/shader/recompiler/Optimization/src/SrtWalker/SrtRuntimeValidator.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtRuntimeValidator.cpp index 9fdf4415a4..eccca9155d 100644 --- a/core/shader/recompiler/Optimization/src/SrtWalker/SrtRuntimeValidator.cpp +++ b/core/shader/recompiler/Optimization/src/SrtWalker/SrtRuntimeValidator.cpp @@ -54,7 +54,7 @@ bool RuntimeValidator::Validate(IrValue* raw, bool requireUniform) { } } if (!requireUniform) return finish(ValidateArguments(inst, false)); - if (_activeMask != nullptr && IsRuntimeSelect(op) && inst.ArgumentCount() == 3 && inst.Argument(0)->Resolve() == _activeMask) { + if (_activeMask != nullptr && IsRuntimeSelect(op) && inst.ArgumentCount() == 3 && ActiveMaskImplies(_activeMask, inst.Argument(0))) { if (_type == RuntimeValueType::Integer && !Validate(inst.Argument(2), false)) { return finish(false); } diff --git a/core/shader/recompiler/Optimization/src/SrtWalker/SrtTape.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtTape.cpp index 5bb86efe50..b18206cf38 100644 --- a/core/shader/recompiler/Optimization/src/SrtWalker/SrtTape.cpp +++ b/core/shader/recompiler/Optimization/src/SrtWalker/SrtTape.cpp @@ -104,7 +104,7 @@ private: } if (value->Opcode() == IrOpcode::Void) return fail(); const auto mask = contexts[context].mask; - if (mask != nullptr && IsRuntimeSelect(value->Opcode()) && value->ArgumentCount() == 3 && value->Argument(0)->Resolve() == mask) return compile(value->Argument(1), context, depth + 1); + if (mask != nullptr && IsRuntimeSelect(value->Opcode()) && value->ArgumentCount() == 3 && ActiveMaskImplies(mask, value->Argument(0))) return compile(value->Argument(1), context, depth + 1); const auto key = std::make_pair(static_cast(value), context); if (const auto found = built.find(key); found != built.end()) return found->second == Building ? fail() : found->second; built.emplace(key, Building); @@ -535,6 +535,7 @@ bool SrtTape::Run(const IrResourcePlan& plan, const SrtRuntime& runtime, std::ve struct Scratch { std::vector active; + std::vector activeFlat; std::vector visited; std::vector pending; std::vector evaluated; @@ -545,6 +546,8 @@ bool SrtTape::Run(const IrResourcePlan& plan, const SrtRuntime& runtime, std::ve auto& active = scratch.active; const auto imageBase = plan.descriptorSources.size(); active.assign(imageBase + plan.info.images.size(), 1u); + auto& activeFlat = scratch.activeFlat; + activeFlat.assign(plan.srtReads.size(), 1u); if (!plan.controlFlow.empty()) { for (const auto& block : plan.controlFlow) { for (const auto source : block.sources) { @@ -555,6 +558,10 @@ bool SrtTape::Run(const IrResourcePlan& plan, const SrtRuntime& runtime, std::ve if (image >= plan.info.images.size()) return refuse(); active[imageBase + image] = 0u; } + for (const auto slot : block.flatSlots) { + if (slot >= activeFlat.size()) return refuse(); + activeFlat[slot] = 0u; + } } auto& visited = scratch.visited; visited.assign(plan.controlFlow.size(), 0u); @@ -569,6 +576,7 @@ bool SrtTape::Run(const IrResourcePlan& plan, const SrtRuntime& runtime, std::ve const auto& block = plan.controlFlow[index]; for (const auto source : block.sources) active[source] = 1u; for (const auto image : block.images) active[imageBase + image] = 1u; + for (const auto slot : block.flatSlots) activeFlat[slot] = 1u; std::uint64_t condition = 0; const bool cleanEvaluable = block.condition != nullptr && runtime.readSpecializationMemory != nullptr && conditionRoots[index] != NoNode && evaluate(frame, conditionRoots[index], condition); if (frame.nullRoot) return refuse(); @@ -601,6 +609,8 @@ bool SrtTape::Run(const IrResourcePlan& plan, const SrtRuntime& runtime, std::ve flattened.assign(plan.srtReads.size(), 0u); for (std::size_t slot = 0; slot < plan.srtReads.size(); ++slot) { const auto& read = plan.srtReads[slot]; + if (read.flatOffset >= activeFlat.size()) return refuse(); + if (activeFlat[read.flatOffset] == 0u) continue; const bool pure = trace != nullptr && read.flatOffset < plan.pureFlatSlots.size() && plan.pureFlatSlots[read.flatOffset] != 0u; if (pure) { trace->leaf = read.value->Resolve(); diff --git a/core/shader/recompiler/Optimization/src/SsaBuilder/SsaPass.cpp b/core/shader/recompiler/Optimization/src/SsaBuilder/SsaPass.cpp index f3ed83cbed..74181182b1 100644 --- a/core/shader/recompiler/Optimization/src/SsaBuilder/SsaPass.cpp +++ b/core/shader/recompiler/Optimization/src/SsaBuilder/SsaPass.cpp @@ -115,7 +115,8 @@ IrValue* Pass::TryRemoveTrivialPhi(IrValue& phi, Variable variable) { if (same == nullptr) { same = MakeInitialValue(variable); } - std::vector users = phi.Uses(); + std::vector users; + for (IrValue* user : phi.Uses()) users.push_back(user); phi.ReplaceUsesWith(same, true); for (IrValue* user : users) { if (user->IsPhi()) { diff --git a/core/shader/recompiler/Optimization/src/TessellationLowering.cpp b/core/shader/recompiler/Optimization/src/TessellationLowering.cpp index 3304659e91..79a616db94 100644 --- a/core/shader/recompiler/Optimization/src/TessellationLowering.cpp +++ b/core/shader/recompiler/Optimization/src/TessellationLowering.cpp @@ -62,6 +62,11 @@ bool IsBase(const IrValue* raw, TessellationBaseKind kind) { return value->Opcode() == IrOpcode::TessellationBase && IsImmediate(value->Argument(0), static_cast(kind)); } +bool OwnPatchRead(const IrValue* value) { + if (SharedAccessOf(value->Opcode()) == SharedAccess::Read) return true; + return BufferAccessOf(value->Opcode()) == BufferAccess::Read && value->ArgumentCount() > 3u && IsBase(value->Argument(3), TessellationBaseKind::OffChip); +} + std::int64_t FloorDiv(std::int64_t value, std::int64_t divisor) { const auto quotient = value / divisor; return quotient * divisor > value ? quotient - 1 : quotient; @@ -351,6 +356,7 @@ private: pending.push_back(value->Argument(1)->Resolve()); continue; } + if (value != start && OwnPatchRead(value)) continue; for (std::size_t index = 0; index < value->ArgumentCount(); ++index) { if (IrValue* argument = value->Argument(index)) pending.push_back(argument->Resolve()); } @@ -558,10 +564,16 @@ std::uint32_t ComponentCount(IrOpcode opcode) { class Lowering { public: - Lowering(IrProgram& program, const ShaderTessellationInputInfo& tessellation) - : program(program), tessellation(tessellation), stage(program.Resources().stage), evaluator(stage == IrShaderStage::TessellationControl ? tessellation.outputControlPoints : 0u, stage == IrShaderStage::Local ? LocalVertexIndexCount : static_cast(tessellation.patchesPerGroup)) {} + Lowering(IrProgram& program, const ShaderTessellationInputInfo& tessellation, const TessellationTargetLimits* limits) + : program(program), tessellation(tessellation), limits(limits), stage(program.Resources().stage), merged(stage == IrShaderStage::TessellationControl && tessellation.mergedHull), + evaluator(stage == IrShaderStage::TessellationControl ? tessellation.outputControlPoints : 0u, stage == IrShaderStage::Local ? LocalVertexIndexCount : static_cast(tessellation.patchesPerGroup)), localStride(program.Metadata().tessellationLocalStride) {} void Run() { + if (merged) { + if (tessellation.inputControlPoints > tessellation.outputControlPoints) Fail("a merged hull program with more input than output control points runs its local part on lanes the hull stage does not have", 0u); + requireUniformBarriers(); + requireOwnLanes(); + } if (stage == IrShaderStage::TessellationControl) lowerOwnLaneBits(); collect(); derive(); @@ -585,12 +597,15 @@ public: } const bool data = usedAsData(relative); if (data) requireGroupedPatches(); + if (data) keepComputedReadsInPatch(*relative.front()); for (IrValue* base : relative) base->ReplaceAllUsesWith(data ? &groupedPatch(*base) : &constant(0u)); deadCodeEliminator.Eliminate(program); if (stage == IrShaderStage::Local && tessellation.drawParameters) writeInstance(); program.Metadata().tessellationLocalStride = localStride; program.Metadata().tessellationControlStride = controlStride; program.Metadata().tessellationPatchStride = patchStride; + program.Metadata().tessellationSharedStride = sharedStride; + if (merged) requireMergedLimits(); } private: @@ -626,6 +641,8 @@ private: if (access.components == 0u || (shared != SharedAccess::Read && shared != SharedAccess::Write)) Fail("only 32-bit LDS reads and writes are supported in tessellation stages", flags.pc); if (stage == IrShaderStage::Local && access.write) { access.kind = TessellationAttribute::LocalOutput; + } else if (merged) { + access.kind = TessellationAttribute::ControlShared; } else if (stage == IrShaderStage::TessellationControl && !access.write) { access.kind = TessellationAttribute::ControlInput; } else { @@ -647,7 +664,7 @@ private: address = memory.offen ? inst->Argument(2) : nullptr; } if (address != nullptr) { - const auto evaluated = evaluator.Evaluate(address, access.predicate, access.kind == TessellationAttribute::ControlInput); + const auto evaluated = evaluator.Evaluate(address, access.predicate, access.kind == TessellationAttribute::ControlInput || access.kind == TessellationAttribute::EvaluationInput); if (!evaluated) Fail("the tessellation ring or LDS address is not affine in the control point and relative patch", flags.pc); access.address = *evaluated; access.addressValue = address; @@ -678,10 +695,16 @@ private: if (access.kind == TessellationAttribute::ControlInput && (access.address.dynamic || access.address.vertex == 0) && access.address.relative > 0 && access.address.relative % inputs == 0) agree(localStride, access.address.relative / inputs, access.pc); if (access.kind == TessellationAttribute::ControlOutput && access.address.vertex != 0) agree(controlStride, access.address.vertex, access.pc); if (access.kind == TessellationAttribute::ControlOutput && soleControlPoint(access)) agree(controlStride, access.address.relative, access.pc); + if (access.kind == TessellationAttribute::ControlShared) { + if (access.address.relative <= 0 || access.address.relative % inputs != 0) Fail("the merged hull program accesses LDS outside its patch", access.pc); + agree(sharedStride, access.address.relative / inputs, access.pc); + } if (access.kind == TessellationAttribute::EvaluationInput) { const auto end = access.address.constant + 4 * static_cast(access.components); if (access.address.relative > 0 && access.address.relative % outputs == 0 && access.address.constant >= 0 && end <= access.address.relative) { agree(controlStride, access.address.relative / outputs, access.pc); + } else if (access.address.dynamic) { + Fail("the domain shader reads off-chip memory at a computed offset outside its patch's control points", access.pc); } else { access.kind = TessellationAttribute::PatchInput; } @@ -720,6 +743,7 @@ private: break; case TessellationAttribute::EvaluationInput: if (address.constant < 0 || end > outputs * controlStride) Fail("the domain shader reads off-chip memory outside its patch's control points", access.pc); + if (address.dynamic && (address.dynamicScale == 0 || address.dynamicScale % controlStride != 0)) Fail("the domain shader reads off-chip memory at a computed offset that is not a whole number of control points", access.pc); break; case TessellationAttribute::Factor: { const bool quads = tessellation.domain == 2u; @@ -734,6 +758,13 @@ private: if (evaluator.Lowest(address) < static_cast(tessellation.patchesPerGroup) * outputs * controlStride) Fail("the domain shader reads off-chip memory that is neither its patch's control points nor a patch-constant record", access.pc); record(access, end); break; + case TessellationAttribute::ControlShared: + if (access.write) { + if (address.vertex != sharedStride || address.constant < 0 || end > sharedStride) Fail("the merged hull program writes LDS outside its own control point's record", access.pc); + } else if ((address.vertex != 0 && address.vertex != sharedStride) || address.constant < 0 || end + address.vertex * (outputs - 1) > inputs * sharedStride) { + Fail("the merged hull program reads LDS outside its patch's control point records", access.pc); + } + break; case TessellationAttribute::PatchOutput: break; } @@ -745,9 +776,12 @@ private: bool dynamicInputAddress(IrValue* user, IrValue* value) { if (user->Opcode() != IrOpcode::GetTessellationAttribute || user->Argument(1)->Resolve() != value->Resolve()) return false; - if (!IsImmediate(user->Argument(0), static_cast(TessellationAttribute::ControlInput))) return false; + const bool control = IsImmediate(user->Argument(0), static_cast(TessellationAttribute::ControlInput)); + const bool evaluation = IsImmediate(user->Argument(0), static_cast(TessellationAttribute::EvaluationInput)); + if (!control && !evaluation) return false; const auto address = evaluator.Evaluate(value, user->Argument(2), true); - return address && address->dynamic && address->relative == static_cast(tessellation.inputControlPoints) * localStride; + const auto stride = control ? static_cast(tessellation.inputControlPoints) * localStride : static_cast(tessellation.outputControlPoints) * controlStride; + return address && address->dynamic && address->relative == stride; } bool usedAsData(const std::vector& bases) { @@ -841,6 +875,66 @@ private: } } + void requireUniformBarriers() const { + const auto& blocks = program.BlockOrder(); + for (IrBlock* block : blocks) { + if (std::none_of(block->Instructions().begin(), block->Instructions().end(), [](const IrValue* inst) { return inst->Opcode() == IrOpcode::Barrier; })) continue; + std::unordered_set seen{block}; + std::vector pending; + if (blocks.front() != block) pending.push_back(blocks.front()); + while (!pending.empty()) { + IrBlock* current = pending.back(); + pending.pop_back(); + if (!seen.insert(current).second) continue; + if (current->Successors().empty()) Fail("the merged hull program has an s_barrier that some invocations do not reach", 0u); + pending.insert(pending.end(), current->Successors().begin(), current->Successors().end()); + } + seen = {}; + pending.assign(block->Successors().begin(), block->Successors().end()); + while (!pending.empty()) { + IrBlock* current = pending.back(); + pending.pop_back(); + if (current == block) Fail("the merged hull program has an s_barrier inside a loop", 0u); + if (!seen.insert(current).second) continue; + pending.insert(pending.end(), current->Successors().begin(), current->Successors().end()); + } + } + } + + void requireOwnLanes() const { + for (IrBlock* block : program.BlockOrder()) { + for (const IrValue* inst : block->Instructions()) { + switch (inst->Opcode()) { + case IrOpcode::ReadLane: + case IrOpcode::WriteLane: + case IrOpcode::DppMoveU32: + case IrOpcode::DppUpdateU32: + case IrOpcode::Permlane16U32: + case IrOpcode::SwizzleU32: + case IrOpcode::PermuteU32: + case IrOpcode::BpermuteU32: + Fail("the merged hull program reads or writes another lane's register, which its hull stage does not hold at one lane per invocation", 0u); + default: + break; + } + } + } + } + + void requireMergedLimits() const { + if (limits == nullptr) Fail("the target has no tessellation limits for a merged hull program", 0u); + const auto vectors = [](std::uint32_t bytes) { return (bytes + 15u) / 16u; }; + const auto patch = vectors(patchStride); + const auto perVertex = vectors(controlStride) + vectors(sharedStride); + const auto locations = vectors(controlStride) + patch + vectors(sharedStride); + const auto total = 4ull * (static_cast(tessellation.outputControlPoints) * perVertex + patch); + if (4u * locations > limits->maxControlPerVertexOutputComponents || 4u * patch > limits->maxControlPerPatchOutputComponents || total > limits->maxControlTotalOutputComponents || 4u > limits->maxControlPerVertexInputComponents) { + char text[256]; + std::snprintf(text, sizeof(text), "the merged hull program's outputs exceed the device limits (%u per-vertex locations, %u patch vectors, %llu components)", locations, patch, static_cast(total)); + Fail(text, 0u); + } + } + void requireFirstInvocation(const Access& access) { if (tessellation.outputControlPoints == 1u) return; std::unordered_set path; @@ -895,12 +989,34 @@ private: auto address = access.address.constant + 4 * static_cast(component); if (access.kind == TessellationAttribute::PatchOutput || access.kind == TessellationAttribute::PatchInput) address -= FloorDiv(address, patchStride) * patchStride; IrValue& offset = constant(static_cast(address)); - if (access.kind != TessellationAttribute::ControlInput || access.address.vertex == 0) return offset; + const bool indexed = access.kind == TessellationAttribute::ControlInput || (access.kind == TessellationAttribute::ControlShared && !access.write); + if (!indexed || access.address.vertex == 0) return offset; IrValue& invocation = insert(access, IrOpcode::GetBuiltin, IrType::U32, {&constant(static_cast(StageInputKind::InvocationId)), &constant(0u)}); IrValue& scaled = insert(access, IrOpcode::IMul32, IrType::U32, {&invocation, &constant(static_cast(access.address.vertex))}); return insert(access, IrOpcode::IAdd32, IrType::U32, {&scaled, &offset}); } + void keepComputedReadsInPatch(IrValue& base) { + for (IrBlock* block : program.BlockOrder()) { + for (IrValue* inst : block->Instructions()) { + if (inst->Opcode() != IrOpcode::GetTessellationAttribute) continue; + const bool control = IsImmediate(inst->Argument(0), static_cast(TessellationAttribute::ControlInput)); + if (!control && !IsImmediate(inst->Argument(0), static_cast(TessellationAttribute::EvaluationInput))) continue; + const auto address = evaluator.Evaluate(inst->Argument(1), inst->Argument(2), true); + if (!address || !address->dynamic || address->relative == 0) continue; + IrValue& patch = program.CreateValue(IrOpcode::IMul32, IrType::U32); + patch.AddArgument(&base); + patch.AddArgument(&constant(static_cast(address->relative))); + block->InsertInstructionBefore(inst, &patch); + IrValue& inPatch = program.CreateValue(IrOpcode::ISub32, IrType::U32); + inPatch.AddArgument(inst->Argument(1)); + inPatch.AddArgument(&patch); + block->InsertInstructionBefore(inst, &inPatch); + inst->ReplaceArgument(1, &inPatch); + } + } + } + void rewrite(const Access& access) { IrValue& kind = constant(static_cast(access.kind)); const bool shared = SharedAccessOf(access.inst->Opcode()) != SharedAccess::None; @@ -954,20 +1070,23 @@ private: IrProgram& program; const ShaderTessellationInputInfo& tessellation; + const TessellationTargetLimits* limits; IrShaderStage stage; + bool merged; AddressEvaluator evaluator; std::vector accesses; - std::uint32_t localStride = 0; + std::uint32_t localStride; std::uint32_t controlStride = 0; std::uint32_t patchStride = 0; + std::uint32_t sharedStride = 0; }; } -void TessellationLowering::Lower(IrProgram& program, const ShaderTessellationInputInfo& tessellation) const { +void TessellationLowering::Lower(IrProgram& program, const ShaderTessellationInputInfo& tessellation, const TessellationTargetLimits* limits) const { const auto stage = program.Resources().stage; if (stage != IrShaderStage::Local && stage != IrShaderStage::TessellationControl && stage != IrShaderStage::TessellationEvaluation) return; - Lowering(program, tessellation).Run(); + Lowering(program, tessellation, limits).Run(); } } diff --git a/core/shader/recompiler/Optimization/src/VectorWindowStoreEliminator.cpp b/core/shader/recompiler/Optimization/src/VectorWindowStoreEliminator.cpp new file mode 100644 index 0000000000..32248e72f9 --- /dev/null +++ b/core/shader/recompiler/Optimization/src/VectorWindowStoreEliminator.cpp @@ -0,0 +1,78 @@ +#include "Optimization/VectorWindowStoreEliminator.hpp" +#include +#include + +namespace ShaderRecompiler { + +namespace { + +using LiveSlots = std::vector; + +const IrValue* constantSlot(const IrValue* inst, std::size_t slots) { + const IrValue* index = inst->Argument(0)->Resolve(); + return index->HasImmediate() && index->ImmediateU32() < slots ? index : nullptr; +} + +void transfer(const IrValue* inst, LiveSlots& live) { + if (inst->Opcode() == IrOpcode::LoadVectorWindow) { + if (const IrValue* slot = constantSlot(inst, live.size())) live[slot->ImmediateU32()] = true; + else live.assign(live.size(), true); + } else if (inst->Opcode() == IrOpcode::StoreVectorWindow) { + if (const IrValue* slot = constantSlot(inst, live.size())) live[slot->ImmediateU32()] = false; + } +} + +} + +VectorWindowStoreEliminationStats VectorWindowStoreEliminator::Eliminate(IrProgram& program) const { + VectorWindowStoreEliminationStats stats; + const std::size_t slots = program.Metadata().vectorWindowSize; + if (slots == 0u) return stats; + const auto& order = program.BlockOrder(); + std::unordered_map liveIn; + for (const IrBlock* block : order) liveIn.emplace(block, LiveSlots(slots, false)); + const auto liveOut = [&](const IrBlock* block) { + LiveSlots live(slots, false); + for (const IrBlock* successor : block->Successors()) { + const auto found = liveIn.find(successor); + if (found == liveIn.end()) return LiveSlots(slots, true); + for (std::size_t slot = 0; slot < slots; ++slot) live[slot] = live[slot] || found->second[slot]; + } + return live; + }; + for (bool changed = true; changed;) { + changed = false; + for (auto block = order.rbegin(); block != order.rend(); ++block) { + auto live = liveOut(*block); + const auto& instructions = (*block)->Instructions(); + for (auto inst = instructions.rbegin(); inst != instructions.rend(); ++inst) transfer(*inst, live); + auto& entry = liveIn[*block]; + if (live != entry) { + entry = std::move(live); + changed = true; + } + } + } + for (IrBlock* block : order) { + auto live = liveOut(block); + auto& instructions = block->Instructions(); + for (auto inst = instructions.end(); inst != instructions.begin();) { + --inst; + IrValue* value = *inst; + if (value->Opcode() == IrOpcode::StoreVectorWindow) { + const IrValue* slot = constantSlot(value, slots); + if (slot != nullptr && !live[slot->ImmediateU32()]) { + value->Invalidate(); + inst = instructions.erase(inst); + value->SetParent(nullptr); + ++stats.removedStores; + continue; + } + } + transfer(value, live); + } + } + return stats; +} + +} diff --git a/core/shader/recompiler/PipelineSpecialization.hpp b/core/shader/recompiler/PipelineSpecialization.hpp index b8088c6afd..ae9ec178cf 100644 --- a/core/shader/recompiler/PipelineSpecialization.hpp +++ b/core/shader/recompiler/PipelineSpecialization.hpp @@ -36,6 +36,8 @@ inline constexpr std::uint32_t WorkgroupOffsetBase = 8241u; inline constexpr std::uint32_t WorkgroupOffsetWords = 3u; inline constexpr std::uint32_t HeapCountBase = 8256u; inline constexpr std::uint32_t MipCountBase = 8320u; +inline constexpr std::uint32_t StoreQuantizeBase = 12288u; +inline constexpr std::uint32_t StoreQuantizeWords = 4u; inline constexpr std::uint32_t DescriptorIndexBase = 16384u; inline constexpr std::uint32_t DescriptorIndexStride = 128u; inline constexpr std::uint32_t FoldOffsetBase = 57344u; diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp index f64c3c6bd7..e3be618395 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp @@ -70,6 +70,7 @@ constexpr ImageOpcodeInfo imageOpcodes[] = { {0xa5u, RdnaOpcode::ImageSample, "image_sample_b_a", RdnaImageSampleFlagBias | RdnaImageSampleFlagAdjust, true, false, false}, {0xa8u, RdnaOpcode::ImageSample, "image_sample_c_a", RdnaImageSampleFlagCompare | RdnaImageSampleFlagAdjust, true, false, false}, {0xb0u, RdnaOpcode::ImageSample, "image_sample_o_a", RdnaImageSampleFlagOffset | RdnaImageSampleFlagAdjust, true, false, false}, + {0xb8u, RdnaOpcode::ImageSample, "image_sample_c_o_a", RdnaImageSampleFlagCompare | RdnaImageSampleFlagOffset | RdnaImageSampleFlagAdjust, true, false, false}, {0x3cu, RdnaOpcode::ImageSampleCLO, nullptr, RdnaImageSampleFlagCompare | RdnaImageSampleFlagLod | RdnaImageSampleFlagOffset, true, false, false}, {0x3fu, RdnaOpcode::ImageSampleCLzO, nullptr, RdnaImageSampleFlagCompare | RdnaImageSampleFlagLevelZero | RdnaImageSampleFlagOffset, true, false, false}, {0x3au, RdnaOpcode::ImageSampleCDO, nullptr, RdnaImageSampleFlagCompare | RdnaImageSampleFlagDerivative | RdnaImageSampleFlagOffset, true, false, false}, diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp index dd297de461..02ccb674b8 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp @@ -1935,7 +1935,7 @@ bool supportsNativeVop3SourceModifiers(RdnaOpcode opcode) { } bool supportsNativeVop3ResultModifiers(RdnaOpcode opcode) { - if (isVop1FloatResultOpcode(opcode) || isNativeVop3F16TernaryOpcode(opcode)) { + if (isVop1FloatResultOpcode(opcode) || isNativeVop3F16TernaryOpcode(opcode) || isCubeOpcode(opcode)) { return true; } switch (opcode) { @@ -1990,7 +1990,7 @@ bool supportsNativeVop3Clamp(RdnaOpcode opcode) { return true; } return supportsNativeVop3ResultModifiers(opcode) || usesInexactClampControl(opcode) || usesIntegerSaturateClamp(opcode) || - isVop3Integer16AddSubOpcode(opcode) || isF32DivisionStepOpcode(opcode) || isCubeOpcode(opcode); + isVop3Integer16AddSubOpcode(opcode) || isF32DivisionStepOpcode(opcode); } void checkNativeVop3Modifiers(RdnaOpcode opcode, bool permlane, bool carryInOut, bool scalarDst, std::uint32_t abs, std::uint32_t opSel, std::uint32_t clamp, std::uint32_t omod, std::uint32_t neg) { diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 028f329dda..ad94823d64 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -40,6 +40,7 @@ #include "Optimization/include/Optimization/ResourceMaterializer.hpp" #include "Optimization/ResourceProgram.hpp" #include "Optimization/include/Optimization/ResourceTracker.hpp" +#include "Optimization/include/Optimization/VectorWindowStoreEliminator.hpp" #include "Optimization/include/Optimization/ShaderInfoCollector.hpp" #include "Optimization/include/Optimization/SrtWalker.hpp" #include "Optimization/include/Optimization/SsaBuilder.hpp" @@ -86,11 +87,17 @@ ShaderStageKind toShaderStageKind(ShaderStage stage) { namespace { +bool MergedHullStage(const RecompileRequest& request) { + const auto stage = request.shader.stage; + return (stage == ShaderStage::Local || stage == ShaderStage::TessellationControl) && request.graphics && request.graphics->tessellation && request.graphics->tessellation->mergedHull; +} + // The host subgroup width wave64 programs are laid out for. Debug aid: APS5_SINGLE_LANE= keeps the listed programs at one guest lane per invocation. std::uint32_t HostSubgroupSize(const RecompileRequest& request) { const bool vertexStage = request.shader.stage == ShaderStage::Vertex || request.shader.stage == ShaderStage::Local; if (vertexStage && !request.target.vertexSubgroups) return 1u; + if (request.shader.stage == ShaderStage::TessellationControl && MergedHullStage(request)) return 1u; static const std::string list = [] { const char* text = std::getenv("APS5_SINGLE_LANE"); return text ? std::string(text) : std::string(); }(); if (!list.empty()) { if (list == "all") return 64u; @@ -101,10 +108,16 @@ std::uint32_t HostSubgroupSize(const RecompileRequest& request) { return request.target.subgroupSize; } -RdnaProgram DecodeStageProgram(ShaderStageKind stage, std::span code) { +RdnaProgram DecodeStageProgram(ShaderStageKind stage, std::span code, bool mergedHull) { + static constexpr std::array EndProgram{0xbf810000u}; + if (stage == ShaderStageKind::Local && mergedHull) { + RdnaProgram program; + program.code = EndProgram; + program.instructions.push_back(DecodeRdnaInstruction(0u, EndProgram, 0u)); + return program; + } if (stage != ShaderStageKind::Local) return RdnaInstructionDecoder{}.Decode(code); auto program = DecodeRdnaFrontProgram(code); - static constexpr std::array EndProgram{0xbf810000u}; program.instructions.back() = DecodeRdnaInstruction(program.instructions.back().programCounter, EndProgram, 0u); return program; } @@ -122,6 +135,18 @@ ShaderStageInputInfo RequestInputInfo(const RecompileRequest& request) { return BuildShaderStageInputInfo(toShaderStageKind(request.shader.stage), request.context, HostSubgroupSize(request), mesh, tessellation); } +void ValidateRequestInputs(const RecompileRequest& request) { + const auto stage = toShaderStageKind(request.shader.stage); + const bool vertexFamily = stage == ShaderStageKind::Vertex || stage == ShaderStageKind::Local || stage == ShaderStageKind::TessellationControl || stage == ShaderStageKind::TessellationEvaluation || stage == ShaderStageKind::Mesh; + if (!vertexFamily) { + static_cast(RequestInputInfo(request)); + return; + } + const auto* mesh = request.graphics && request.graphics->mesh ? &*request.graphics->mesh : nullptr; + const auto* tessellation = request.graphics && request.graphics->tessellation ? &*request.graphics->tessellation : nullptr; + ValidateVertexStageInputs(stage, request.context, mesh, tessellation); +} + std::uint32_t DeviceMemoryLdsBytes(const RecompileRequest& request) { if (!request.context.compute.has_value()) return 0u; const auto bytes = static_cast(request.context.compute->ldsSizeDwords) * 4u; @@ -134,8 +159,8 @@ std::atomic frontEndBuilds{0}; struct PreparedControlFlow { PreparedControlFlow(const RecompileRequest& request, const SwappcInfo& swappcInfo) - : stage(request.shader.stage), fallbacks(swappcInfo.linkedCallFallbacks.begin(), swappcInfo.linkedCallFallbacks.end()), swappc(WithFallbacks(swappcInfo, fallbacks)), code(request.shader.code.begin(), request.shader.code.end()), - decoded(DecodeStageProgram(toShaderStageKind(request.shader.stage), code)), cfg(GraphBuilder{}.Build(decoded, &swappc)) { + : stage(request.shader.stage), mergedHull(MergedHullStage(request)), fallbacks(swappcInfo.linkedCallFallbacks.begin(), swappcInfo.linkedCallFallbacks.end()), swappc(WithFallbacks(swappcInfo, fallbacks)), code(request.shader.code.begin(), request.shader.code.end()), + decoded(DecodeStageProgram(toShaderStageKind(request.shader.stage), code, mergedHull)), cfg(GraphBuilder{}.Build(decoded, &swappc)) { Structurizer{}.Structurize(cfg); } @@ -143,7 +168,7 @@ struct PreparedControlFlow { PreparedControlFlow& operator=(const PreparedControlFlow&) = delete; bool Matches(const RecompileRequest& request, const SwappcInfo& swappcInfo) const { - return stage == request.shader.stage && swappc.fetchCallAllowed == swappcInfo.fetchCallAllowed && + return stage == request.shader.stage && mergedHull == MergedHullStage(request) && swappc.fetchCallAllowed == swappcInfo.fetchCallAllowed && swappc.userDataBaseRegister == swappcInfo.userDataBaseRegister && swappc.userDataCount == swappcInfo.userDataCount && std::ranges::equal(fallbacks, swappcInfo.linkedCallFallbacks) && std::ranges::equal(code, request.shader.code); } @@ -154,6 +179,7 @@ struct PreparedControlFlow { } ShaderStage stage; + bool mergedHull; std::vector fallbacks; SwappcInfo swappc; std::vector code; @@ -218,8 +244,9 @@ IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparat constexpr InstructionTranslator translator; + if (frontend->mergedHull && cfg.hasFetchCall) throw std::runtime_error("a merged hull program that calls a fetch subroutine is unsupported"); EmbeddedFetchPlan embeddedFetch; - if ((stageKind == ShaderStageKind::Vertex || stageKind == ShaderStageKind::Local) && inputInfo.vertex != nullptr && inputInfo.vertex->fetchEmbedded) { + if ((stageKind == ShaderStageKind::Vertex || stageKind == ShaderStageKind::Local) && !frontend->mergedHull && inputInfo.vertex != nullptr && inputInfo.vertex->fetchEmbedded) { embeddedFetch = EmbeddedVertexFetchAnalyzer{}.Analyze(decoded, inputInfo.vertex->fetchAttribReg, inputInfo.vertex->fetchBufferReg, request.context.userDataBaseRegister, static_cast(request.context.userData.size()), request.context.waveSize); translateOptions.embeddedFetch = &embeddedFetch; } @@ -239,6 +266,7 @@ IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparat constexpr DeadCodeEliminator deadCodeEliminator; constexpr ReadLaneEliminator readLaneEliminator; constexpr MaskedSelectEliminator maskedSelectEliminator; + constexpr VectorWindowStoreEliminator vectorWindowStoreEliminator; const auto simplify = [&] { dumpIr("translate"); ssaBuilder.Rewrite(program); @@ -258,6 +286,10 @@ IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparat deadCodeEliminator.Eliminate(program); } + if (vectorWindowStoreEliminator.Eliminate(program).removedStores != 0u) { + deadCodeEliminator.Eliminate(program); + } + if (maskedSelectEliminator.Eliminate(program).removedSelects != 0u) { const bool liveAncillary = std::ranges::any_of(program.Blocks(), [](const auto& block) { return std::ranges::any_of(block->Instructions(), [](const IrValue* value) { @@ -290,7 +322,7 @@ IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparat if (inputInfo.vertex != nullptr) { constexpr TessellationLowering tessellationLowering; - tessellationLowering.Lower(program, inputInfo.vertex->tess); + tessellationLowering.Lower(program, inputInfo.vertex->tess, request.target.tessellation ? &*request.target.tessellation : nullptr); } constexpr SrtWalker srtWalker; @@ -571,6 +603,41 @@ CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program) targetOptions.spannedWaves = request.target.spannedWaves; targetOptions.robustBufferAccess2 = request.target.robustBufferAccess2; targetOptions.float32FlushToZero = request.target.float32FlushToZero; + targetOptions.float32DenormPreserve = request.target.float32DenormPreserve; + targetOptions.meshBarycentricPixel = request.shader.stage == ShaderStage::Fragment && request.graphics && request.graphics->mesh; + if (request.shader.stage == ShaderStage::Mesh && request.graphics && request.graphics->meshBarycentrics) { + const auto& paired = *request.graphics->meshBarycentrics; + const auto emulated = DescribeEmulatedInterface(paired.parameters, paired.emulation, true); + const auto& layout = emulated.layout; + targetOptions.meshBarycentricVertex = true; + targetOptions.barycentricSmoothLocation = layout.smoothLocation; + targetOptions.barycentricLinearLocation = layout.linearLocation; + targetOptions.barycentricPositionsLocation = layout.positionsLocation; + targetOptions.meshExportedLocations = emulated.passed; + for (const auto& parameter : emulated.perVertex) targetOptions.meshPerVertexLocations.emplace_back(parameter.sourceLocation, parameter.location); + if (!request.graphics->mesh || !request.target.mesh) throw std::runtime_error("barycentric emulation on the mesh path needs a mesh configuration and a device with mesh shaders"); + const auto& config = *request.graphics->mesh; + const auto& limits = *request.target.mesh; + if (config.provokingVertex != 0u) throw std::runtime_error("barycentric emulation on the mesh path is only measured with the first provoking vertex"); + const std::uint64_t perVertexParameters = emulated.perVertex.size(); + const std::uint64_t sharedBytes = 16ull * config.maxVertices * (1u + perVertexParameters) + DeviceMemoryLdsBytes(request); + if (sharedBytes > limits.maxSharedMemoryBytes) throw std::runtime_error("barycentric emulation on the mesh path needs " + std::to_string(sharedBytes) + " bytes of mesh shared memory for " + std::to_string(config.maxVertices) + " vertices, the device allows " + std::to_string(limits.maxSharedMemoryBytes)); + MeshOutputUse use; + std::set flatLocations; + for (const auto& output : program.Info().outputs) { + if (output.kind == StageOutputKind::Parameter && std::ranges::find(targetOptions.meshExportedLocations, output.location) != targetOptions.meshExportedLocations.end()) flatLocations.insert(output.location); + if (output.kind == StageOutputKind::ClipDistance) use.clipDistances = std::max(use.clipDistances, output.index + 1u); + if (output.kind == StageOutputKind::CullDistance) use.cullDistances = std::max(use.cullDistances, output.index + 1u); + if (output.kind == StageOutputKind::Layer) use.layer = true; + } + use.flatParameters = static_cast(flatLocations.size()); + use.perVertexParameters = static_cast(perVertexParameters); + use.clipVaryings = emulated.barycentrics; + const auto budget = MeshBarycentricOutputBudget(config, limits, use); + if (budget.components > limits.maxOutputComponents) throw std::runtime_error("barycentric emulation on the mesh path needs " + std::to_string(budget.components) + " mesh output components (" + std::to_string(budget.vertexLocations) + " per-vertex and " + std::to_string(budget.primitiveLocations) + " per-primitive locations), the device allows " + std::to_string(limits.maxOutputComponents)); + if (budget.bytes > limits.maxOutputMemoryBytes) throw std::runtime_error("barycentric emulation on the mesh path needs " + std::to_string(budget.bytes) + " bytes of mesh output memory, the device allows " + std::to_string(limits.maxOutputMemoryBytes)); + } + targetOptions.waveLdsMemoryBarriers = request.target.waveLdsMemoryBarriers; constexpr SpirvEmitter spirvEmitter; CompiledShaderArtifact result; @@ -591,9 +658,10 @@ CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program) result.spirv = spirvEmitter.Emit(program, inputInfo, bindings, targetOptions); } result.registrationModule = registration; + if (request.shader.stage == ShaderStage::Mesh && request.graphics) result.meshBarycentrics = request.graphics->meshBarycentrics; result.stencilReplaySafe = (request.shader.stage == ShaderStage::Vertex || stencilReplay) && StencilReplaySafe(program); if (stencilReplay) { - result.stencilReferences = StencilReferenceValues(program); + result.stencilReferences = StencilReferenceValues(program, request.context.pixel.has_value() && request.context.pixel->stencilOperationValueExport ? 8u : 0u); result.stencilReferenceDword = bindings.layout.stencilReferenceDword; } @@ -935,11 +1003,16 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil .parameterExports = artifact.parameterExports, .fragmentParameters = artifact.fragmentParameters, .barycentricEmulation = artifact.barycentricEmulation, + .meshBarycentrics = artifact.meshBarycentrics, .stencilReferences = artifact.stencilReferences, .stencilReplaySafe = artifact.stencilReplaySafe, .stencilReferenceDword = artifact.stencilReferenceDword, .variantId = artifact.variantId }; + for (const auto reg : variant.bindings.layout.userDataRegisters) { + const auto index = reg - request.context.userDataBaseRegister; + result.userDataRegisterReads |= reg >= request.context.userDataBaseRegister && index < 64u ? std::uint64_t{1} << index : ~std::uint64_t{0}; + } const auto plan = preparedBindingPlan(variant, snapshot, request.context.pixel ? std::span(request.context.pixel->targetExportMapping) : std::span{}, request.target.registrationModules); const DescriptorBindingPlan* bindingPlan = &plan->bindings; std::shared_ptr entry; @@ -1094,6 +1167,26 @@ bool sameLayout(const BindingLayout& left, const BindingLayout& right) { } +MeshOutputBudget MeshBarycentricOutputBudget(const MeshConfiguration& mesh, const MeshTargetLimits& limits, const MeshOutputUse& use) { + const auto alignUp = [](std::uint64_t value, std::uint64_t granularity) { return granularity <= 1u ? value : (value + granularity - 1u) / granularity * granularity; }; + MeshOutputBudget budget; + budget.vertexLocations = 1u + use.flatParameters + use.clipVaryings + (use.clipDistances + 3u) / 4u + (use.cullDistances + 3u) / 4u; + budget.primitiveLocations = 3u + 3u * use.perVertexParameters + 1u + (use.layer ? 1u : 0u); + budget.components = 4ull * (budget.vertexLocations + budget.primitiveLocations); + budget.bytes = 16ull * (budget.vertexLocations * alignUp(mesh.maxVertices, limits.outputPerVertexGranularity) + budget.primitiveLocations * alignUp(mesh.maxPrimitives, limits.outputPerPrimitiveGranularity)); + return budget; +} + +std::optional MeshBarycentricLayoutOf(const RecompileResult& fragment) { + if (!fragment.barycentricEmulation.active) return std::nullopt; + MeshBarycentricLayout layout; + layout.emulation = fragment.barycentricEmulation; + for (const auto& parameter : fragment.fragmentParameters) { + layout.parameters.push_back({parameter.location, parameter.sourceLocation, false, parameter.perVertex}); + } + return layout; +} + struct PreparedInterface { static CompiledVariant Compile(SourceEntry& source, const RecompileRequest& request, ShaderPreparationContext& preparation, std::exception_ptr* emissionFailure) { if (preparation.interfaceSource.get() != &source || preparation.interfaceProgram->Resources().shaderHash != request.shader.codeAddress) { @@ -1527,7 +1620,7 @@ std::optional PreparedShaderInvocation::TryCreate(cons std::optional PreparedShaderInvocation::TryCreate(const RecompileRequest& request, const std::shared_ptr& handle, std::span key) { if (handle == nullptr || !MatchesPreparedShader(request, *handle, key)) return std::nullopt; - if (request.shader.stage != ShaderStage::Compute && request.shader.stage != ShaderStage::Fragment) static_cast(RequestInputInfo(request)); + if (request.shader.stage != ShaderStage::Compute && request.shader.stage != ShaderStage::Fragment) ValidateRequestInputs(request); return PreparedShaderInvocation(request, handle); } @@ -1581,7 +1674,7 @@ std::shared_ptr CaptureResources(const RecompileRequest& static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; const auto started = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; // The whole vertex family (Vertex, Local, TC, TE, Mesh) validates V# fields the memo key does not cover. - if (request.shader.stage != ShaderStage::Compute && request.shader.stage != ShaderStage::Fragment) static_cast(RequestInputInfo(request)); + if (request.shader.stage != ShaderStage::Compute && request.shader.stage != ShaderStage::Fragment) ValidateRequestInputs(request); auto capture = std::make_shared(); capture->source = handle.source; capture->plan = acquirePlan(request, handle.source, true, &handle); diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index c90d72a599..127ed88d5b 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -180,6 +180,7 @@ struct ShaderPixelStageInfo { bool dualSourceBlend; bool alphaToCoverage; bool stencilRefExportEnable; + bool stencilOperationValueExport = false; RasterPrimitive rasterPrimitive = RasterPrimitive::Triangle; bool layerExported = false; bool ditheredAlphaToCoverage = false; @@ -189,6 +190,7 @@ struct ShaderPixelStageInfo { bool iteratesSamples = false; PixelPositionLocation positionLocation = PixelPositionLocation::Center; std::array sampleLocations{}; + std::array sampleOrder{}; }; struct ShaderVertexBufferResource { @@ -289,6 +291,8 @@ struct SpirvTarget { bool robustBufferAccess2 = false; bool depthRangeRemap = false; bool float32FlushToZero = false; + bool waveLdsMemoryBarriers = false; + bool float32DenormPreserve = false; }; struct BindingLayout { @@ -339,6 +343,7 @@ struct TessellationConfiguration { bool breakAtInstance = false; std::uint32_t instancePatches = 0; bool drawParameters = false; + bool mergedHull = false; }; inline constexpr std::uint32_t MeshDrawPushOffsetBytes = 104; @@ -364,12 +369,37 @@ struct GraphicsDrawParameters { std::uint32_t instanceCount; }; +struct FragmentParameter { + std::uint32_t location; + std::uint32_t sourceLocation; + bool flat; + bool perVertex; + bool custom = false; + bool flatP10 = false; + bool vertexOrder = false; + bool operator==(const FragmentParameter&) const = default; +}; + +struct BarycentricEmulation { + bool active = false; + bool smooth = false; + bool linear = false; + bool operator==(const BarycentricEmulation&) const = default; +}; + +struct MeshBarycentricLayout { + BarycentricEmulation emulation; + std::vector parameters; + bool operator==(const MeshBarycentricLayout&) const = default; +}; + struct GraphicsCompileContext { std::uint32_t firstUserSgpr; std::span linkedPrograms; std::optional mesh; std::optional tessellation; GraphicsDrawParameters draw; + std::optional meshBarycentrics; }; struct RecompileRequest { @@ -473,30 +503,31 @@ struct VertexAttribute { std::uint32_t formatComponents = 0; }; -struct FragmentParameter { - std::uint32_t location; - std::uint32_t sourceLocation; - bool flat; - bool perVertex; - bool custom = false; - bool flatP10 = false; - bool vertexOrder = false; -}; - -struct BarycentricEmulation { - bool active = false; - bool smooth = false; - bool linear = false; -}; - struct BarycentricEmulationLayout { static constexpr std::uint32_t NoLocation = 0xffffffffu; std::uint32_t smoothLocation = NoLocation; std::uint32_t linearLocation = NoLocation; + std::uint32_t positionsLocation = NoLocation; std::vector> perVertexLocations; }; -[[nodiscard]] BarycentricEmulationLayout LayoutBarycentricEmulation(std::span parameters, const BarycentricEmulation& emulation); +[[nodiscard]] BarycentricEmulationLayout LayoutBarycentricEmulation(std::span parameters, const BarycentricEmulation& emulation, bool meshPositions = false); + +struct EmulatedPerVertexParameter { + std::uint32_t sourceLocation; + std::uint32_t location; +}; + +struct EmulatedInterface { + std::vector passed; + std::vector passedFlat; + std::vector perVertex; + BarycentricEmulationLayout layout; + std::uint32_t barycentrics = 0; + std::uint32_t sources = 0; +}; + +[[nodiscard]] EmulatedInterface DescribeEmulatedInterface(std::span parameters, const BarycentricEmulation& emulation, bool meshPositions = false); // A 128-bit hash of a module's words (SharedSpirv::ContentHash), zero for an empty module. The // mixing is pure integer arithmetic over the words in order, so the value is the same on every run, @@ -635,6 +666,7 @@ struct CompiledShaderArtifact { std::vector parameterExports; std::vector fragmentParameters; BarycentricEmulation barycentricEmulation; + std::optional meshBarycentrics; std::vector stencilReferences; bool stencilReplaySafe = false; std::uint32_t stencilReferenceDword = 0xffffffffu; @@ -648,6 +680,7 @@ struct ShaderInvocation { std::vector pushConstants; bool readsDepthRemapPush = false; std::vector vertexAttributes; + std::uint64_t userDataRegisterReads = 0; std::uint32_t poisonedSrtReads = 0; std::vector imageTablePoison; std::vector> imageTableRanges; @@ -695,6 +728,26 @@ struct RecompileResult : CompiledShaderArtifact, ShaderInvocation { } }; +[[nodiscard]] std::optional MeshBarycentricLayoutOf(const RecompileResult& fragment); + +struct MeshOutputUse { + std::uint32_t flatParameters = 0; + std::uint32_t perVertexParameters = 0; + std::uint32_t clipVaryings = 0; + std::uint32_t clipDistances = 0; + std::uint32_t cullDistances = 0; + bool layer = false; +}; + +struct MeshOutputBudget { + std::uint32_t vertexLocations = 0; + std::uint32_t primitiveLocations = 0; + std::uint64_t components = 0; + std::uint64_t bytes = 0; +}; + +[[nodiscard]] MeshOutputBudget MeshBarycentricOutputBudget(const MeshConfiguration& mesh, const MeshTargetLimits& limits, const MeshOutputUse& use); + [[nodiscard]] RecompileResult Recompile(const RecompileRequest& request); struct ResourceCapture; @@ -731,7 +784,7 @@ struct BarycentricTessellationShaders { RecompileResult evaluation; }; -[[nodiscard]] BarycentricTessellationShaders BuildBarycentricTessellationShaders(const RecompileResult& vertex, const RecompileResult& fragment, const SpirvTarget& target, std::uint32_t maxFragmentInputComponents); +[[nodiscard]] BarycentricTessellationShaders BuildBarycentricTessellationShaders(const RecompileResult& vertex, const RecompileResult& fragment, const SpirvTarget& target, std::uint32_t maxFragmentInputComponents, std::uint32_t provokingVertex); } 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 f830045b52..bcda9f0c40 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -84,9 +84,9 @@ std::filesystem::path ShaderCacheDirectory() { namespace ShaderRecompiler::ShaderDiskCache { #if defined(__linux__) && defined(__x86_64__) && defined(__GLIBCXX__) -static_assert(sizeof(CompiledShaderArtifact) == 240, "CompiledShaderArtifact changed: update the artifact encoder"); -static_assert(sizeof(ShaderInvocation) == 208, "ShaderInvocation changed: update the invocation encoder"); -static_assert(sizeof(RecompileResult) == 480, "RecompileResult changed: update EncodeResult and DecodeResult"); +static_assert(sizeof(CompiledShaderArtifact) == 280, "CompiledShaderArtifact changed: update the artifact encoder"); +static_assert(sizeof(ShaderInvocation) == 216, "ShaderInvocation changed: update the invocation encoder"); +static_assert(sizeof(RecompileResult) == 528, "RecompileResult changed: update EncodeResult and DecodeResult"); static_assert(sizeof(DescriptorBinding) == 656, "DescriptorBinding changed: update the binding encoder"); static_assert(sizeof(VertexAttribute) == 32, "VertexAttribute changed: update the attribute encoder"); static_assert(sizeof(VertexInput) == 16, "VertexInput changed: update the vertex input encoder"); @@ -105,13 +105,13 @@ static_assert(sizeof(BindingAllocationResult) == 184, "BindingAllocationResult c static_assert(sizeof(CompiledBindingLayout) == 88, "CompiledBindingLayout changed: update the allocation encoder"); static_assert(sizeof(BindingLayout) == 16, "BindingLayout changed: update BuildKey"); static_assert(sizeof(IrResourcePlan) == 728, "IrResourcePlan changed: update EncodePlan and DecodePlan"); -static_assert(sizeof(IrValue) == 152, "IrValue changed: update the plan value encoder"); +static_assert(sizeof(IrValue) == 128, "IrValue changed: update the plan value encoder"); static_assert(sizeof(GuestRegister) == 8, "GuestRegister changed: update the plan value encoder"); static_assert(sizeof(MemoryInfo) == 80, "MemoryInfo changed: update the plan encoder"); static_assert(sizeof(DescriptorSource) == 192, "DescriptorSource changed: update the plan encoder"); static_assert(sizeof(TableColumn) == 112, "TableColumn changed: update the plan encoder"); static_assert(sizeof(KeyDomain) == 16, "KeyDomain changed: update the plan encoder"); -static_assert(sizeof(ResourceBlock) == 80, "ResourceBlock changed: update the plan encoder"); +static_assert(sizeof(ResourceBlock) == 104, "ResourceBlock changed: update the plan encoder"); static_assert(sizeof(SrtRead) == 16, "SrtRead changed: update the plan encoder"); static_assert(sizeof(UniformFill) == 28, "UniformFill changed: update the plan encoder"); static_assert(sizeof(UniformFillPlan) == 64, "UniformFillPlan changed: update the plan encoder"); @@ -140,7 +140,7 @@ constexpr std::size_t fieldCount() { static_assert(fieldCount() == 23, "ShaderInfo changed: update the info encoder"); static_assert(fieldCount() == 12, "BufferResource changed: update the info encoder"); -static_assert(fieldCount() == 39, "ImageResource changed: update the info encoder"); +static_assert(fieldCount() == 41, "ImageResource changed: update the info encoder"); static_assert(fieldCount() == 8, "SamplerResource changed: update the info encoder"); static_assert(fieldCount() == 3, "SampledResourcePair changed: update the info encoder"); static_assert(fieldCount() == 5, "StageInput changed: update the info encoder"); @@ -151,7 +151,7 @@ static_assert(fieldCount() == 31, "MemoryInfo changed: update the pl static_assert(fieldCount() == 3, "DescriptorSource changed: update the plan encoder"); static_assert(fieldCount() == 18, "TableColumn changed: update the plan encoder"); static_assert(fieldCount() == 4, "KeyDomain changed: update the plan encoder"); -static_assert(fieldCount() == 4, "ResourceBlock changed: update the plan encoder"); +static_assert(fieldCount() == 5, "ResourceBlock changed: update the plan encoder"); static_assert(fieldCount() == 2, "SrtRead changed: update the plan encoder"); static_assert(fieldCount() == 5, "UniformFill changed: update the plan encoder"); static_assert(fieldCount() == 2, "UniformFillPlan changed: update the plan encoder"); @@ -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()) { @@ -504,6 +507,17 @@ void encodeArtifact(Writer& writer, const CompiledShaderArtifact& result) { writer.Value(result.barycentricEmulation.active); writer.Value(result.barycentricEmulation.smooth); writer.Value(result.barycentricEmulation.linear); + writer.Value(result.meshBarycentrics.has_value()); + if (result.meshBarycentrics) { + writer.Value(result.meshBarycentrics->emulation.active); + writer.Value(result.meshBarycentrics->emulation.smooth); + writer.Value(result.meshBarycentrics->emulation.linear); + writer.List(result.meshBarycentrics->parameters, [](Writer& out, const FragmentParameter& parameter) { + out.Value(parameter.location); + out.Value(parameter.sourceLocation); + out.Value(parameter.perVertex); + }); + } writer.Values(std::span(result.stencilReferences)); writer.Value(result.stencilReplaySafe); writer.Value(result.stencilReferenceDword); @@ -551,6 +565,21 @@ void decodeArtifact(Reader& reader, CompiledShaderArtifact& result) { reader.Value(result.barycentricEmulation.active); reader.Value(result.barycentricEmulation.smooth); reader.Value(result.barycentricEmulation.linear); + bool hasMeshBarycentrics = false; + reader.Value(hasMeshBarycentrics); + result.meshBarycentrics.reset(); + if (hasMeshBarycentrics) { + MeshBarycentricLayout layout; + reader.Value(layout.emulation.active); + reader.Value(layout.emulation.smooth); + reader.Value(layout.emulation.linear); + reader.List(layout.parameters, 9, [](Reader& in, FragmentParameter& parameter) { + in.Value(parameter.location); + in.Value(parameter.sourceLocation); + in.Value(parameter.perVertex); + }); + result.meshBarycentrics = std::move(layout); + } reader.Values(result.stencilReferences); reader.Value(result.stencilReplaySafe); reader.Value(result.stencilReferenceDword); @@ -573,6 +602,7 @@ void encodeInvocation(Writer& writer, const ShaderInvocation& invocation) { out.Value(attribute.fetchIndex); out.Value(attribute.formatComponents); }); + writer.Value(invocation.userDataRegisterReads); writer.Value(invocation.poisonedSrtReads); writer.List(invocation.imageTablePoison, [](Writer& out, const ImageTableEntryPoison& poison) { for (const auto word : poison.words) out.Value(word); @@ -613,6 +643,7 @@ void decodeInvocation(Reader& reader, ShaderInvocation& invocation) { in.Value(attribute.fetchIndex); in.Value(attribute.formatComponents); }); + reader.Value(invocation.userDataRegisterReads); reader.Value(invocation.poisonedSrtReads); reader.List(invocation.imageTablePoison, 44, [](Reader& in, ImageTableEntryPoison& poison) { for (auto& word : poison.words) in.Value(word); @@ -711,10 +742,12 @@ void encodeShaderInfo(Writer& writer, const ShaderInfo& info) { out.Value(image.cubeFaceCompatible); out.Value(image.flatVolumeCompatible); out.Value(image.singleMipPlaneBiasCompatible); + out.Value(image.volumeArraySampleCompatible); out.Value(image.flatLineCompatible); out.Value(image.flatVolumeLineCompatible); out.Value(image.foldTexelOffsetsOnly); out.Value(image.convertedSamples); + out.Value(image.remappedDepth1D); out.Value(image.byElements); out.Value(image.byComponents); out.Value(image.packedFormat); @@ -814,10 +847,12 @@ void decodeShaderInfo(Reader& reader, ShaderInfo& info) { in.Value(image.cubeFaceCompatible); in.Value(image.flatVolumeCompatible); in.Value(image.singleMipPlaneBiasCompatible); + in.Value(image.volumeArraySampleCompatible); in.Value(image.flatLineCompatible); in.Value(image.flatVolumeLineCompatible); in.Value(image.foldTexelOffsetsOnly); in.Value(image.convertedSamples); + in.Value(image.remappedDepth1D); in.Value(image.byElements); in.Value(image.byComponents); in.Value(image.packedFormat); @@ -1067,6 +1102,7 @@ bool encodePlan(Writer& writer, const IrResourcePlan& plan) { out.Values(std::span(block.successors)); out.Values(std::span(block.sources)); out.Values(std::span(block.images)); + out.Values(std::span(block.flatSlots)); }); writer.Values(std::span(plan.materializationSources)); writer.List(plan.srtReads, [&](Writer& out, const SrtRead& read) { @@ -1224,11 +1260,12 @@ bool decodePlan(Reader& reader, IrResourcePlan& plan) { in.Value(column.selection); source.tableColumn = table ? std::optional(std::move(column)) : std::nullopt; }); - reader.List(plan.controlFlow, 28, [&](Reader& in, ResourceBlock& block) { + reader.List(plan.controlFlow, 36, [&](Reader& in, ResourceBlock& block) { block.condition = valueAt(in.Get()); in.Values(block.successors); in.Values(block.sources); in.Values(block.images); + in.Values(block.flatSlots); }); reader.Values(plan.materializationSources); reader.List(plan.srtReads, 8, [&](Reader& in, SrtRead& read) { @@ -1262,7 +1299,8 @@ bool decodePlan(Reader& reader, IrResourcePlan& plan) { return (block.condition == nullptr || block.successors.size() == 2u) && std::ranges::all_of(block.successors, [&](std::uint32_t successor) { return successor < plan.controlFlow.size(); }) && std::ranges::all_of(block.sources, knownSource) && - std::ranges::all_of(block.images, [&](std::uint32_t image) { return image < plan.info.images.size(); }); + std::ranges::all_of(block.images, [&](std::uint32_t image) { return image < plan.info.images.size(); }) && + std::ranges::all_of(block.flatSlots, [&](std::uint32_t slot) { return slot < plan.srtReads.size(); }); }); if (!blocksValid) return false; ResourceMaterializer::CompactPlan(plan); @@ -1444,17 +1482,33 @@ private: entries += pack->Count(); packs.push_back(std::move(pack)); }; - for (const auto& path : PrebuiltCachePaths(root)) { + for (const auto& candidate : PrebuiltCachePaths(root)) { std::error_code error; +#ifdef _WIN32 + const auto path = NativeCachePath_nid_no_patch(candidate, error); + if (error) continue; +#else + const auto& path = candidate; +#endif if (std::filesystem::is_regular_file(path, error)) { addPack(path); continue; } if (!std::filesystem::is_directory(path, error)) continue; bool loose = false; std::vector packPaths; - for (std::filesystem::directory_iterator it(path, error), end; !error && it != end; it.increment(error)) { - const auto extension = it->path().extension(); - if (extension == ".pack") packPaths.push_back(it->path()); - if (extension == ".bin" && it->path().stem().string().size() == 32) loose = true; + const auto collect = [&](const std::filesystem::path& entry) { + const auto extension = entry.extension(); + if (extension == ".pack") packPaths.push_back(entry); + if (extension == ".bin" && entry.stem().string().size() == 32) loose = true; + }; +#ifdef _WIN32 + WIN32_FIND_DATAW data{}; + const auto handle = FindFirstFileW((path / L"*").c_str(), &data); + if (handle != INVALID_HANDLE_VALUE) { + struct Search { HANDLE handle; ~Search() { FindClose(handle); } } search{handle}; + do { collect(path / data.cFileName); } while (FindNextFileW(handle, &data)); } +#else + for (std::filesystem::directory_iterator it(path, error), end; !error && it != end; it.increment(error)) collect(it->path()); +#endif std::sort(packPaths.begin(), packPaths.end()); for (const auto& item : packPaths) addPack(item); if (loose) looseDirectories.push_back(path); @@ -1615,6 +1669,11 @@ private: const auto stamp = directory / "last-used"; std::ofstream(stamp, std::ios::binary | std::ios::trunc).close(); std::filesystem::last_write_time(stamp, std::filesystem::file_time_type::clock::now(), error); +#ifdef _WIN32 + auto nativeRoot = root; + nativeRoot.make_preferred(); + if (nativeRoot.native().starts_with(L"\\\\?\\") || nativeRoot.native().starts_with(L"\\\\.\\")) return; +#endif const auto now = std::filesystem::file_time_type::clock::now(); for (std::filesystem::directory_iterator it(root, error), end; !error && it != end; it.increment(error)) { const auto name = it->path().filename().string(); diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index d009bb2dae..e5acfa71f9 100644 --- a/core/shader/recompiler/ShaderDiskCache.hpp +++ b/core/shader/recompiler/ShaderDiskCache.hpp @@ -13,7 +13,7 @@ namespace ShaderRecompiler::ShaderDiskCache { -inline constexpr std::uint32_t FormatVersion = 40; +inline constexpr std::uint32_t FormatVersion = 44; enum class LoadStatus { Loaded, 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/include/SpirvBackend/SpirvEmitter.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp index f32c6b2545..8c8c56b4ad 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp @@ -29,6 +29,15 @@ struct SpirvTargetOptions { bool spannedWaves = false; bool robustBufferAccess2 = false; bool float32FlushToZero = false; + bool float32DenormPreserve = false; + bool meshBarycentricPixel = false; + bool meshBarycentricVertex = false; + std::uint32_t barycentricSmoothLocation = 0xffffffffu; + std::uint32_t barycentricLinearLocation = 0xffffffffu; + std::uint32_t barycentricPositionsLocation = 0xffffffffu; + std::vector meshExportedLocations; + std::vector> meshPerVertexLocations; + bool waveLdsMemoryBarriers = false; }; struct SpirvEmitInfo { diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp index df8a86348e..96951cf815 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp @@ -276,7 +276,7 @@ std::uint32_t AtomicUpdateTyped(SpirvEmitterState& state, std::uint32_t pointer, EmitLabel(state, cont); state.module.AddFunction(spv::OpBranch, header); EmitLabel(state, merge); - state.module.AddFunction(spv::OpMemoryBarrier, ConstantU32(state, scope), ConstantU32(state, spv::MemorySemanticsAcquireReleaseMask | memory)); + if (kind != ResourceKind::Lds || !state.insideLdsLock) state.module.AddFunction(spv::OpMemoryBarrier, ConstantU32(state, scope), ConstantU32(state, spv::MemorySemanticsAcquireReleaseMask | memory)); return observed; } diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp index 1a1beae999..a8f03e192a 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp @@ -348,6 +348,7 @@ std::uint32_t EmitSharedAtomicMskor64(SpirvValueEmitContext& ctx, const IrValue& std::uint32_t EmitDataAppend(SpirvValueEmitContext& ctx, const IrValue& inst); std::uint32_t EmitDataConsume(SpirvValueEmitContext& ctx, const IrValue& inst); std::uint32_t EmitDataOrderedCount(SpirvValueEmitContext& ctx, const IrValue& inst); +std::uint32_t EmitDataOrderedSwap(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitImageQueryDimensions(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitImageQueryLod(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitImageRead(SpirvValueEmitContext& ctx, const IrValue& inst); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index b328b2b343..dc3a268ee3 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -94,7 +94,7 @@ struct SpirvEmitterState { SpirvModule module; const IrProgram& program; ShaderStageInputInfo inputInfo; - std::array tessVariables {}; + std::array tessVariables {}; std::uint32_t tessInnerVariable = 0; SpirvRequirements requirements; @@ -112,6 +112,22 @@ struct SpirvEmitterState { bool float16DenormPreserve = false; bool robustBufferAccess2 = false; bool float32FlushToZero = false; + bool float32DenormPreserve = false; + bool meshBarycentricPixel = false; + bool meshBarycentricVertex = false; + std::uint32_t barycentricSmoothLocation = 0xffffffffu; + std::uint32_t barycentricLinearLocation = 0xffffffffu; + std::uint32_t barycentricPositionsLocation = 0xffffffffu; + std::vector meshExportedLocations; + std::vector> meshPerVertexLocations; + std::vector> meshVertexParameters; + std::uint32_t meshPositionStaging = 0; + std::uint32_t meshClipSmooth = 0; + std::uint32_t meshClipLinear = 0; + std::uint32_t meshFlatPositions = 0; + std::uint32_t pixelFlatPositions = 0; + std::map, std::uint32_t> solvedBarycentrics; + bool waveLdsMemoryBarriers = false; std::unordered_set sharedLaneValues; std::uint32_t laneHalf = 0; std::uint32_t spirvVersion = 0x00010300u; @@ -164,6 +180,7 @@ struct SpirvEmitterState { // Execution scope of the barriers that keep one guest wave's LDS accesses in program order across // host invocations (see WaveLdsScope); 0 when none are emitted. std::uint32_t waveLdsScope = 0; + bool insideLdsLock = false; // Debug aid (APS5_LOOP_GUARD=): after that many evaluated loop exits an invocation leaves its // loops, and the first loop that ran out is reported as a LoopLimit fault instead of hanging the GPU. std::uint32_t loopGuardLimit = 0; @@ -205,6 +222,7 @@ struct SpirvEmitterState { std::uint32_t quadPixelUncovered = 0; std::uint32_t sampleMaskInputVariable = 0; std::uint32_t quadSampleCoverage = 0; + std::uint32_t sampleOrderWord = 0; std::uint32_t subgroupLocalInvocationIdVariable = 0; std::uint32_t subgroupIdVariable = 0; std::uint32_t perVertexVariable = 0; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvModule.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvModule.hpp index 17e535a57b..a6a4b13296 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvModule.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvModule.hpp @@ -142,6 +142,7 @@ public: private: static constexpr std::size_t DeclarationCacheSize = 512; + static constexpr std::size_t DeclarationCacheProbes = 8; static constexpr std::size_t MaxCachedDeclarationWords = 4; struct DeclarationSlot { @@ -163,10 +164,11 @@ private: std::uint32_t interned(const std::uint32_t* words, std::uint32_t length, TDeclare&& declare) { const auto start = declarationCacheSlot(words, length); std::size_t target = start; - for (std::size_t probe = 0; probe < DeclarationCacheSize; ++probe) { - target = (start + probe) & (DeclarationCacheSize - 1u); - auto& slot = declarationCache[target]; + for (std::size_t probe = 0; probe < DeclarationCacheProbes; ++probe) { + const auto index = (start + probe) & (DeclarationCacheSize - 1u); + auto& slot = declarationCache[index]; if (slot.id == 0) { + target = index; break; } if (slot.length == length && std::equal(words, words + length, slot.words.begin())) { diff --git a/core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp b/core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp index dffc7835fa..6f86243daf 100644 --- a/core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp +++ b/core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp @@ -20,41 +20,14 @@ void require(bool condition, const std::string& message) { if (!condition) throw std::runtime_error("Barycentric geometry SPIR-V: " + message); } -struct PerVertexParameter { - std::uint32_t sourceLocation; - std::uint32_t location; -}; - -struct EmulatedInterface { - std::vector passed; - std::vector passedFlat; - std::vector perVertex; - BarycentricEmulationLayout layout; - std::uint32_t barycentrics = 0; - std::uint32_t sources = 0; -}; +using PerVertexParameter = EmulatedPerVertexParameter; EmulatedInterface CollectEmulatedInterface(const RecompileResult& vertex, const RecompileResult& fragment) { require(fragment.barycentricEmulation.active, "the fragment shader does not emulate barycentrics"); - const auto exported = [&](std::uint32_t location) { return std::find(vertex.parameterExports.begin(), vertex.parameterExports.end(), location) != vertex.parameterExports.end(); }; - EmulatedInterface emulated; - emulated.layout = LayoutBarycentricEmulation(fragment.fragmentParameters, fragment.barycentricEmulation); for (const auto& parameter : fragment.fragmentParameters) { - require(exported(parameter.sourceLocation), "fragment parameter " + std::to_string(parameter.location) + " is not exported by the vertex shader"); - if (!parameter.perVertex) { - emulated.passed.push_back(parameter.sourceLocation); - emulated.passedFlat.push_back(parameter.flat); - continue; - } - const auto relocated = std::find_if(emulated.layout.perVertexLocations.begin(), emulated.layout.perVertexLocations.end(), [&](const auto& entry) { return entry.first == parameter.location; }); - require(relocated != emulated.layout.perVertexLocations.end(), "per-vertex parameter " + std::to_string(parameter.location) + " has no emulated location"); - emulated.perVertex.push_back({parameter.sourceLocation, relocated->second}); + require(std::find(vertex.parameterExports.begin(), vertex.parameterExports.end(), parameter.sourceLocation) != vertex.parameterExports.end(), "fragment parameter " + std::to_string(parameter.location) + " is not exported by the vertex shader"); } - emulated.barycentrics = (emulated.layout.smoothLocation != BarycentricEmulationLayout::NoLocation ? 1u : 0u) + (emulated.layout.linearLocation != BarycentricEmulationLayout::NoLocation ? 1u : 0u); - std::set sources(emulated.passed.begin(), emulated.passed.end()); - for (const auto& parameter : emulated.perVertex) sources.insert(parameter.sourceLocation); - emulated.sources = static_cast(sources.size()); - return emulated; + return DescribeEmulatedInterface(fragment.fragmentParameters, fragment.barycentricEmulation); } class BarycentricGeometryEmitter { @@ -253,7 +226,7 @@ public: return finish(); } - std::vector EmitEvaluation(const std::vector& sources, const std::vector& passed, const std::vector& passedFlat, const std::vector& perVertex, const BarycentricEmulationLayout& layout) { + std::vector EmitEvaluation(const std::vector& sources, const std::vector& passed, const std::vector& passedFlat, const std::vector& perVertex, const BarycentricEmulationLayout& layout, std::uint32_t provokingVertex) { begin(); addDomain(); const auto tessCoord = addInterface(spv::StorageClassInput, vec3FloatType); @@ -299,7 +272,7 @@ public: store(result(spv::OpAccessChain, ptrOutputVec4, glOut, constant(intType, 0u)), load(vec4FloatType, result(spv::OpAccessChain, ptrInputVec4, glIn, index, constant(intType, 0u)))); if (clipDistances != 0u) store(result(spv::OpAccessChain, ptrEvaluationOutputClip, glOut, constant(intType, 1u)), load(clipArrayType, result(spv::OpAccessChain, ptrEvaluationInputClip, glIn, index, constant(intType, 1u)))); - for (std::size_t i = 0; i < passed.size(); ++i) store(passedOutputs[i], load(vec4FloatType, result(spv::OpAccessChain, ptrInputVec4, inputs[passed[i]], passedFlat[i] ? constant(intType, 0u) : index))); + for (std::size_t i = 0; i < passed.size(); ++i) store(passedOutputs[i], load(vec4FloatType, result(spv::OpAccessChain, ptrInputVec4, inputs[passed[i]], passedFlat[i] ? constant(intType, provokingVertex) : index))); if (smooth != 0u) store(smooth, weights); if (linear != 0u) store(linear, weights); for (std::size_t i = 0; i < perVertex.size(); ++i) { @@ -378,7 +351,7 @@ private: } -BarycentricEmulationLayout LayoutBarycentricEmulation(std::span parameters, const BarycentricEmulation& emulation) { +BarycentricEmulationLayout LayoutBarycentricEmulation(std::span parameters, const BarycentricEmulation& emulation, bool meshPositions) { BarycentricEmulationLayout layout; if (!emulation.active) return layout; std::array used {}; @@ -396,12 +369,33 @@ BarycentricEmulationLayout LayoutBarycentricEmulation(std::span parameters, const BarycentricEmulation& emulation, bool meshPositions) { + EmulatedInterface emulated; + emulated.layout = LayoutBarycentricEmulation(parameters, emulation, meshPositions); + for (const auto& parameter : parameters) { + if (!parameter.perVertex) { + emulated.passed.push_back(parameter.sourceLocation); + emulated.passedFlat.push_back(parameter.flat); + continue; + } + const auto relocated = std::find_if(emulated.layout.perVertexLocations.begin(), emulated.layout.perVertexLocations.end(), [&](const auto& entry) { return entry.first == parameter.location; }); + require(relocated != emulated.layout.perVertexLocations.end(), "per-vertex parameter " + std::to_string(parameter.location) + " has no emulated location"); + emulated.perVertex.push_back({parameter.sourceLocation, relocated->second}); + } + emulated.barycentrics = (emulated.layout.smoothLocation != BarycentricEmulationLayout::NoLocation ? 1u : 0u) + (emulated.layout.linearLocation != BarycentricEmulationLayout::NoLocation ? 1u : 0u); + std::set sources(emulated.passed.begin(), emulated.passed.end()); + for (const auto& parameter : emulated.perVertex) sources.insert(parameter.sourceLocation); + emulated.sources = static_cast(sources.size()); + return emulated; +} + RecompileResult BuildBarycentricGeometryShader(const RecompileResult& vertex, const RecompileResult& fragment, const SpirvTarget& target, const std::optional& limits) { require(fragment.barycentricEmulation.active, "the fragment shader does not emulate barycentrics"); require(limits.has_value(), "geometry shaders are unavailable"); @@ -423,8 +417,9 @@ RecompileResult BuildBarycentricGeometryShader(const RecompileResult& vertex, co return result; } -BarycentricTessellationShaders BuildBarycentricTessellationShaders(const RecompileResult& vertex, const RecompileResult& fragment, const SpirvTarget& target, std::uint32_t maxFragmentInputComponents) { +BarycentricTessellationShaders BuildBarycentricTessellationShaders(const RecompileResult& vertex, const RecompileResult& fragment, const SpirvTarget& target, std::uint32_t maxFragmentInputComponents, std::uint32_t provokingVertex) { require(fragment.barycentricEmulation.active, "the fragment shader does not emulate barycentrics"); + require(provokingVertex == 0u || provokingVertex == 2u, "the provoking vertex must be the first or the last of the triangle"); require(target.tessellation.has_value(), "tessellation shaders are unavailable"); require(target.spirvVersion >= 0x00010300u && target.spirvVersion <= 0x00010500u, "unsupported SPIR-V target version"); require(std::find(target.supportedCapabilities.begin(), target.supportedCapabilities.end(), spv::CapabilityTessellation) != target.supportedCapabilities.end(), "tessellation capability is unavailable"); @@ -443,7 +438,7 @@ BarycentricTessellationShaders BuildBarycentricTessellationShaders(const Recompi const std::vector sources(sourceSet.begin(), sourceSet.end()); BarycentricTessellationShaders shaders; shaders.control.spirv = BarycentricTessellationEmitter(target.spirvVersion, clipDistances).EmitControl(sources); - shaders.evaluation.spirv = BarycentricTessellationEmitter(target.spirvVersion, clipDistances).EmitEvaluation(sources, emulated.passed, emulated.passedFlat, emulated.perVertex, emulated.layout); + shaders.evaluation.spirv = BarycentricTessellationEmitter(target.spirvVersion, clipDistances).EmitEvaluation(sources, emulated.passed, emulated.passedFlat, emulated.perVertex, emulated.layout, provokingVertex); #if ANYPS5_ENABLE_SPIRV_TOOLS shaders.control.spirv = ValidateAndOptimizeSpirv(shaders.control.spirv, target.vulkanVersion, target.spirvVersion); shaders.evaluation.spirv = ValidateAndOptimizeSpirv(shaders.evaluation.spirv, target.vulkanVersion, target.spirvVersion); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp index 639c8cb63b..6514d92cc1 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp @@ -75,6 +75,7 @@ bool LaneSource(const IrProgram& program, const IrValue& value) { case IrOpcode::DataConsume: case IrOpcode::LoadVectorWindow: case IrOpcode::DataOrderedCount: + case IrOpcode::DataOrderedSwap: return true; case IrOpcode::GetBuiltin: { const auto* kind = value.Argument(0)->Resolve(); @@ -375,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/SpirvBdaRead.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp index dc1a70f0b4..017a6b7514 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp @@ -607,7 +607,7 @@ std::array EmitAnchoredBdaRead(SpirvEmitterState& state, const const auto pointer = state.module.AllocateId(); state.module.AddFunction(spv::OpConvertUToPtr, bytePointer, pointer, element); const auto loaded = state.module.AllocateId(); - state.module.AddFunction(spv::OpLoad, byteType, loaded, pointer, BdaAccessMask(coherent), 1u); + state.module.AddFunction(spv::OpLoad, byteType, loaded, pointer, BdaAccessMask(coherent || bits == 16u), 1u); value = Binary(state, spv::OpBitwiseOr, u32, value, Binary(state, spv::OpShiftLeftLogical, u32, Unary(state, spv::OpUConvert, u32, loaded), ConstantU32(state, byte * 8u))); } directValues[0] = value; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp index e02fa2611d..17d2db36bf 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 { @@ -260,14 +267,25 @@ std::vector SpirvEmitter::Emit(const IrProgram& program, const Sh state.depthRangeRemap = target.depthRangeRemap; state.registrationModules = target.registrationModules; state.fragmentDescriptorSet = target.fragmentDescriptorSet; + state.meshBarycentricPixel = target.meshBarycentricPixel; + state.meshBarycentricVertex = target.meshBarycentricVertex; + state.barycentricSmoothLocation = target.barycentricSmoothLocation; + state.barycentricLinearLocation = target.barycentricLinearLocation; + state.barycentricPositionsLocation = target.barycentricPositionsLocation; + state.meshExportedLocations = target.meshExportedLocations; + state.meshPerVertexLocations = target.meshPerVertexLocations; state.float16DenormPreserve = target.float16DenormPreserve; state.robustBufferAccess2 = target.robustBufferAccess2; state.float32FlushToZero = target.float32FlushToZero; + state.waveLdsMemoryBarriers = target.waveLdsMemoryBarriers; + state.float32DenormPreserve = target.float32DenormPreserve; state.hostSubgroupSize = target.subgroupSize; if (state.requirements.ldsLock && LdsInDeviceMemory(state) && std::find(target.supportedCapabilities.begin(), target.supportedCapabilities.end(), static_cast(spv::CapabilityInt64Atomics)) != target.supportedCapabilities.end()) { 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/SpirvFlowEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp index a19022f3c7..4cd661b16a 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp @@ -667,6 +667,7 @@ void EmitDirectInstruction(SpirvValueEmitContext& ctx, const IrValue& inst) { case IrOpcode::DataAppend: return Invoke(EmitDataAppend, ctx, inst); case IrOpcode::DataConsume: return Invoke(EmitDataConsume, ctx, inst); case IrOpcode::DataOrderedCount: return Invoke(EmitDataOrderedCount, ctx, inst); + case IrOpcode::DataOrderedSwap: return Invoke(EmitDataOrderedSwap, ctx, inst); case IrOpcode::SwizzleU32: return Invoke(EmitSwizzleU32, ctx, inst); case IrOpcode::ImageQueryDimensions: return Invoke(EmitImageQueryDimensions, ctx, inst); case IrOpcode::ImageQueryLod: return Invoke(EmitImageQueryLod, ctx, inst); @@ -759,7 +760,9 @@ void EmitStructuredBlock(SpirvValueEmitContext& ctx, StructuredFunctionState& fu } else if (access != SharedAccess::None) { const bool writes = access != SharedAccess::Read; if (ldsWritten || (writes && ldsRead)) { - state.module.AddFunction(spv::OpControlBarrier, ConstantU32(state, state.waveLdsScope), ConstantU32(state, spv::ScopeWorkgroup), ConstantU32(state, spv::MemorySemanticsAcquireReleaseMask | LdsMemorySemantics(state))); + const auto semantics = ConstantU32(state, spv::MemorySemanticsAcquireReleaseMask | LdsMemorySemantics(state)); + if (state.waveLdsMemoryBarriers && state.waveLdsScope == spv::ScopeSubgroup) state.module.AddFunction(spv::OpMemoryBarrier, ConstantU32(state, spv::ScopeSubgroup), semantics); + else state.module.AddFunction(spv::OpControlBarrier, ConstantU32(state, state.waveLdsScope), ConstantU32(state, spv::ScopeWorkgroup), semantics); ldsWritten = false; ldsRead = false; } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 57a3c77d68..772f4f8f4c 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -193,9 +193,12 @@ std::uint32_t CoordF32(SpirvValueEmitContext& ctx, const ImageEmitAccess& access x = CubeAxis(ctx.state, x); y = CubeAxis(ctx.state, y); } + if (!gradient && access.image.depthCompare && (access.image.remappedDepth1D || access.mem.imageDimension == RdnaImageDimension::Dim1D || access.mem.imageDimension == RdnaImageDimension::Dim1DArray)) { + y = ConstantF32(ctx.state, 0x3f000000u); + } const auto result = ctx.state.module.AllocateId(); if (components == 3u) { - auto z = component(2u); + auto z = (access.image.depthCompare && (access.mem.imageDimension == RdnaImageDimension::Dim1DArray || (access.image.remappedDepth1D && access.image.dimension == RdnaImageDimension::Dim2DArray))) ? component(1u) : component(2u); if (cube) { z = CubeLayer(ctx.state, z); } @@ -467,6 +470,9 @@ std::uint32_t PackedOffset(SpirvValueEmitContext& ctx, const ImageEmitAccess& ac values[index] = state.module.AllocateId(); state.module.AddFunction(spv::OpBitFieldSExtract, TypeI32(state), values[index], packed, ConstantU32(state, index * 8u), ConstantU32(state, 6)); } + if (access.image.remappedDepth1D && components >= 2u) { + values[1] = ConstantI32(state, 0); + } if (components == 1u) { return values[0]; } @@ -1002,6 +1008,67 @@ std::uint32_t ImageSampleIndex(SpirvValueEmitContext& ctx, const ImageEmitAccess return AddressU32(ctx, access, addressInfo.coordinateComponents); } +std::uint32_t QuantizeStoreTexel(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t texel) { + const auto first = PipelineSpecialization::StoreQuantizeBase + resource * PipelineSpecialization::StoreQuantizeWords; + const auto f32 = TypeF32(state); + const auto vec4 = TypeF32Vector(state, 4u); + const auto bvec4 = state.module.Type(spv::OpTypeVector, TypeBool(state), 4u); + const auto splat = [&](std::uint32_t type, std::uint32_t scalar) { + const auto result = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, type, result, scalar, scalar, scalar, scalar); + return result; + }; + const auto word = [&](std::uint32_t index) { return state.module.SpecializationConstant(TypeU32(state), first + index, 0u); }; + const auto enabled = Binary(state, spv::OpINotEqual, TypeBool(state), word(0u), ConstantU32(state, 0u)); + const auto scale = splat(vec4, Unary(state, spv::OpBitcast, f32, word(1u))); + const auto reciprocal = splat(vec4, Unary(state, spv::OpBitcast, f32, word(2u))); + const auto snorm = Binary(state, spv::OpINotEqual, TypeBool(state), word(3u), ConstantU32(state, 0u)); + const auto lower = splat(vec4, Select(state, f32, snorm, ConstantF32(state, std::bit_cast(-1.0f)), ConstantF32(state, 0u))); + const auto zero = splat(vec4, ConstantF32(state, 0u)); + const auto one = splat(vec4, ConstantF32(state, std::bit_cast(1.0f))); + const auto finite = state.module.AllocateId(); + state.module.AddFunction(spv::OpSelect, vec4, finite, Unary(state, spv::OpIsNan, bvec4, texel), zero, texel); + const auto clamped = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, vec4, clamped, GlslStd450(state), GLSLstd450FClamp, finite, lower, one); + const auto product = Binary(state, spv::OpFMul, vec4, clamped, scale); + state.module.AddAnnotation(spv::OpDecorate, product, spv::DecorationNoContraction); + const auto ext = [&](GLSLstd450 op, const auto&... operands) { + const auto result = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, vec4, result, GlslStd450(state), static_cast(op), operands...); + return result; + }; + const auto exact = [&](spv::Op opcode, std::uint32_t lhs, std::uint32_t rhs) { + const auto value = Binary(state, opcode, vec4, lhs, rhs); + state.module.AddAnnotation(spv::OpDecorate, value, spv::DecorationNoContraction); + return value; + }; + const auto spreadFactor = splat(vec4, ConstantF32(state, std::bit_cast(4097.0f))); + const auto high = [&](std::uint32_t value) { + const auto spread = exact(spv::OpFMul, value, spreadFactor); + return exact(spv::OpFSub, spread, exact(spv::OpFSub, spread, value)); + }; + const auto clampedHigh = high(clamped); + const auto scaleHigh = high(scale); + const auto clampedLow = exact(spv::OpFSub, clamped, clampedHigh); + const auto scaleLow = exact(spv::OpFSub, scale, scaleHigh); + auto error = exact(spv::OpFSub, exact(spv::OpFMul, clampedHigh, scaleHigh), product); + error = exact(spv::OpFAdd, error, exact(spv::OpFMul, clampedHigh, scaleLow)); + error = exact(spv::OpFAdd, error, exact(spv::OpFMul, clampedLow, scaleHigh)); + error = exact(spv::OpFAdd, error, exact(spv::OpFMul, clampedLow, scaleLow)); + const auto below = ext(GLSLstd450Floor, product); + const auto fraction = Binary(state, spv::OpFSub, vec4, product, below); + state.module.AddAnnotation(spv::OpDecorate, fraction, spv::DecorationNoContraction); + const auto tie = Binary(state, spv::OpFOrdEqual, bvec4, fraction, splat(vec4, ConstantF32(state, std::bit_cast(0.5f)))); + const auto above = Binary(state, spv::OpFAdd, vec4, below, one); + const auto even = ext(GLSLstd450RoundEven, product); + const auto broken = Select(state, vec4, Binary(state, spv::OpFOrdGreaterThan, bvec4, error, zero), above, Select(state, vec4, Binary(state, spv::OpFOrdLessThan, bvec4, error, zero), below, even)); + const auto level = Select(state, vec4, tie, broken, even); + const auto quantized = Binary(state, spv::OpFMul, vec4, level, reciprocal); + const auto result = state.module.AllocateId(); + state.module.AddFunction(spv::OpSelect, vec4, result, splat(bvec4, enabled), quantized, texel); + return result; +} + void EmitWriteOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) { auto& state = ctx.state; const bool uintImage = access.image.numericClass == IrTextureNumericClass::Uint; @@ -1015,7 +1082,8 @@ void EmitWriteOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) { EmitIfCondition(state, condition, [&]() { const auto mipLod = access.image.mipMode == ImageMipMode::DynamicStorage ? LodU32(ctx, access) : 0u; const auto coord = CoordU32(ctx, access); - const auto texel = access.mem.imagePacked ? PackedStoreTexel(ctx, access, ctx.Arg(access.inst, 2)) : StoreTexel(ctx, access, ctx.Arg(access.inst, 2), uintImage); + auto texel = access.mem.imagePacked ? PackedStoreTexel(ctx, access, ctx.Arg(access.inst, 2)) : StoreTexel(ctx, access, ctx.Arg(access.inst, 2), uintImage); + if (!uintImage && !access.mem.imagePacked && !access.image.atomic && access.image.table == NoTable && !state.registrationModules && access.image.conversionFormat == IrBufferFormat::Invalid) texel = QuantizeStoreTexel(state, access.mem.resource, texel); EmitStorageImageWrite(state, access.mem.resource, mipLod, coord, texel, ImageSampleIndex(ctx, access)); }); } @@ -1197,8 +1265,8 @@ void EmitGatherOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, con if (setup.dref && (access.image.emulatedCompare & EmulatedCompare::Enabled) != 0u) { ctx.Fail(access.inst, "is a comparison gather of a color texture, which is not implemented"); } - if (dimension == RdnaImageDimension::Dim1D || dimension == RdnaImageDimension::Dim1DArray) { - if (setup.dref || !HasFlag(mem, RdnaImageSampleFlagLevelZero) || HasFlag(mem, RdnaImageSampleFlagGatherHorizontal)) { + if (dimension == RdnaImageDimension::Dim1D || dimension == RdnaImageDimension::Dim1DArray || access.image.remappedDepth1D) { + if (setup.dref || !HasFlag(mem, RdnaImageSampleFlagLevelZero) || HasFlag(mem, RdnaImageSampleFlagGatherHorizontal) || access.image.remappedDepth1D) { ctx.Fail(access.inst, "has an unsupported 1D gather variant"); } const auto sample = EmitOneDimensionalGatherLz(ctx, access, setup); @@ -1483,6 +1551,9 @@ void EmitSampleOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, con const auto field = (bits >> (index * 8u)) & 0x3fu; values[index] = ConstantI32(state, static_cast(field ^ 0x20u) - 0x20); } + if (access.image.remappedDepth1D && setup.dimensionInfo.spatialComponents >= 2u) { + values[1] = ConstantI32(state, 0); + } const auto count = setup.dimensionInfo.spatialComponents; const auto offset = count == 1u ? values[0] : count == 2u ? state.module.Constant(spv::OpConstantComposite, TypeI32Vector(state, 2), values[0], values[1]) diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp index c6f3526911..a8068f0415 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp @@ -218,7 +218,14 @@ void DefineInputs(SpirvEmitterState& state) { const auto reads = [&](StageInputKind kind) { return std::any_of(state.inputs.begin(), state.inputs.end(), [&](const SpirvInputBinding& input) { return input.kind == kind; }); }; - emulation = LayoutBarycentricEmulation(parameters, {true, reads(StageInputKind::BaryCoordSmooth), reads(StageInputKind::BaryCoordNoPerspective)}); + emulation = LayoutBarycentricEmulation(parameters, {true, reads(StageInputKind::BaryCoordSmooth), reads(StageInputKind::BaryCoordNoPerspective)}, state.meshBarycentricPixel); + if (state.meshBarycentricPixel && emulation.positionsLocation != BarycentricEmulationLayout::NoLocation) { + state.pixelFlatPositions = DefineInterfaceVariable(state, state.module.Type(spv::OpTypeArray, TypeF32Vector(state, 4u), ConstantU32(state, 3u)), spv::StorageClassInput, "meshBarycentricPositions"); + state.module.AddAnnotation(spv::OpDecorate, state.pixelFlatPositions, spv::DecorationLocation, emulation.positionsLocation); + state.module.AddAnnotation(spv::OpDecorate, state.pixelFlatPositions, spv::DecorationPerPrimitiveEXT); + state.module.EmitCapability(spv::CapabilityMeshShadingEXT); + state.module.EmitExtension("SPV_EXT_mesh_shader"); + } } for (auto& input : state.inputs) { if (pixelStage && input.kind == StageInputKind::Parameter) { @@ -294,7 +301,7 @@ void DefineInputs(SpirvEmitterState& state) { throw std::runtime_error("SPIR-V module emission failed: per-vertex parameter " + std::to_string(location) + " has no emulated location"); } location = relocated->second; - state.module.AddAnnotation(spv::OpDecorate, input.variableId, spv::DecorationFlat); + state.module.AddAnnotation(spv::OpDecorate, input.variableId, state.meshBarycentricPixel ? spv::DecorationPerPrimitiveEXT : spv::DecorationFlat); } else if (input.perVertex) { state.module.AddAnnotation(spv::OpDecorate, input.variableId, spv::DecorationPerVertexKHR); } else if (flat) { @@ -411,8 +418,8 @@ void DefineOutputs(SpirvEmitterState& state) { break; case StageOutputKind::StencilRef: if (std::ranges::find(state.supportedCapabilities, static_cast(spv::CapabilityStencilExportEXT)) == state.supportedCapabilities.end()) { - const auto values = StencilReferenceValues(state.program); - if (values.empty()) throw std::runtime_error("stencil reference export emulation requires a proven set of at most 16 values"); + const auto values = StencilReferenceValues(state.program, PixelInfo(state).psStencilOperationValueExport ? 8u : 0u); + 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; @@ -472,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/SpirvMemoryInstructions.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp index b022fd463f..d997a174eb 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp @@ -916,7 +916,7 @@ std::uint32_t Atomic32(SpirvValueEmitContext& ctx, const IrValue& inst, const Me const auto old = EmitAtomicOperation(ctx, inst, pointer, scope); if (lds) { const std::uint32_t semantics = spv::MemorySemanticsAcquireReleaseMask | LdsMemorySemantics(state); - state.module.AddFunction(spv::OpMemoryBarrier, ConstantU32(state, scope), ConstantU32(state, semantics)); + if (!state.insideLdsLock) state.module.AddFunction(spv::OpMemoryBarrier, ConstantU32(state, scope), ConstantU32(state, semantics)); } else { EmitDeviceAtomicMemoryBarrier(state); } @@ -1035,7 +1035,7 @@ std::uint32_t SharedAtomicUpdate(SpirvValueEmitContext& ctx, const IrValue& inst if (InFunctionStorage(state, mem.kind)) return UpdateFunctionElement(state, pointer, next); const auto old = AtomicUpdate(state, pointer, mem.kind, next); const std::uint32_t semantics = spv::MemorySemanticsAcquireReleaseMask | LdsMemorySemantics(state); - state.module.AddFunction(spv::OpMemoryBarrier, ConstantU32(state, spv::ScopeWorkgroup), ConstantU32(state, semantics)); + if (!state.insideLdsLock) state.module.AddFunction(spv::OpMemoryBarrier, ConstantU32(state, spv::ScopeWorkgroup), ConstantU32(state, semantics)); return old; }); } @@ -1301,7 +1301,7 @@ std::uint32_t AddressAtomic(SpirvValueEmitContext& ctx, const IrValue& inst) { }); } -std::uint32_t AppendConsume(SpirvValueEmitContext& ctx, const IrValue& inst, bool append, bool ordered = false) { +std::uint32_t AppendConsume(SpirvValueEmitContext& ctx, const IrValue& inst, bool append, bool ordered = false, bool swap = false) { auto& state = ctx.state; if (ctx.half == 1u) { if (ctx.otherHalf == nullptr) { @@ -1342,10 +1342,10 @@ std::uint32_t AppendConsume(SpirvValueEmitContext& ctx, const IrValue& inst, boo const auto condition = AndCondition(state, isFirst, AndCondition(state, lanesActive, bounds)); const auto atomic = EmitValueOrZeroIfCondition(state, condition, [&]() { if (InFunctionStorage(state, mem.kind)) { - return UpdateFunctionElement(state, EmitMemoryElementPointer(state, access, index), [&](std::uint32_t old) { return Binary(state, append ? spv::OpIAdd : spv::OpISub, TypeU32(state), old, count); }); + return UpdateFunctionElement(state, EmitMemoryElementPointer(state, access, index), [&](std::uint32_t old) { return swap ? amount : Binary(state, append ? spv::OpIAdd : spv::OpISub, TypeU32(state), old, count); }); } const auto value = state.module.AllocateId(); - state.module.AddFunction(append ? spv::OpAtomicIAdd : spv::OpAtomicISub, TypeU32(state), value, EmitMemoryElementPointer(state, access, index), ConstantU32(state, gds ? spv::ScopeDevice : spv::ScopeWorkgroup), ConstantU32(state, spv::MemorySemanticsMaskNone), amount); + state.module.AddFunction(swap ? spv::OpAtomicExchange : append ? spv::OpAtomicIAdd : spv::OpAtomicISub, TypeU32(state), value, EmitMemoryElementPointer(state, access, index), ConstantU32(state, gds ? spv::ScopeDevice : spv::ScopeWorkgroup), ConstantU32(state, spv::MemorySemanticsMaskNone), amount); return value; }); const auto result = state.module.AllocateId(); @@ -1392,8 +1392,11 @@ std::uint32_t LockedLdsUpdate(SpirvEmitterState& state, std::uint32_t type, std: state.module.AddFunction(spv::OpSelectionMerge, attemptMerge, spv::SelectionControlMaskNone); state.module.AddFunction(spv::OpBranchConditional, acquired, critical, attemptMerge); EmitLabel(state, critical); + state.insideLdsLock = true; const auto old = update(); - state.module.AddFunction(spv::OpAtomicStore, lock, ConstantU32(state, spv::ScopeWorkgroup), ConstantU32(state, spv::MemorySemanticsReleaseMask | LdsMemorySemantics(state)), ConstantU32(state, 0u)); + state.insideLdsLock = false; + const auto released = state.module.AllocateId(); + state.module.AddFunction(spv::OpAtomicExchange, TypeU32(state), released, lock, ConstantU32(state, spv::ScopeWorkgroup), ConstantU32(state, spv::MemorySemanticsReleaseMask | LdsMemorySemantics(state)), ConstantU32(state, 0u)); const auto criticalExit = state.currentLabel; state.module.AddFunction(spv::OpBranch, attemptMerge); EmitLabel(state, attemptMerge); @@ -1556,14 +1559,14 @@ std::uint32_t EmitReadConst(SpirvValueEmitContext& ctx, const IrValue& inst) { const auto guard = static_cast(found - guarded.begin()); const auto flag = load(ConstantU32(state, resources.srtGuardOffset + guard)); const auto poisoned = Binary(state, spv::OpINotEqual, TypeBool(state), flag, ConstantU32(state, 0u)); + const auto records = resources.srtGuardOffset + static_cast(guarded.size()); + if (records + 3u * guarded.size() >= (1u << BdaAbi::SrtGuardStageShift)) { + ctx.Fail(inst, "has more guarded SRT records than the fault buffer's guard word can name"); + } EmitIfCondition(state, poisoned, [&] { - const auto records = resources.srtGuardOffset + static_cast(guarded.size()); const auto record = Binary(state, spv::OpIAdd, u32, ConstantU32(state, records), Binary(state, spv::OpIMul, u32, Binary(state, spv::OpISub, u32, flag, ConstantU32(state, 1u)), ConstantU32(state, 3u))); - const auto word = [&](std::uint32_t offset) { return load(Binary(state, spv::OpIAdd, u32, record, ConstantU32(state, offset))); }; - const auto pc = word(0u); - const auto low = word(1u); - const auto high = word(2u); - RecordBdaFaultWords(state, low, high, ConstantU32(state, 4u), pc, BdaAbi::FaultReason::Unmapped); + const auto stage = ConstantU32(state, static_cast(resources.stage) << BdaAbi::SrtGuardStageShift); + state.module.AddFunction(spv::OpAtomicStore, BdaWord(state, state.faultBufferVariable, ConstantU32(state, BdaAbi::SrtGuardWord)), ConstantU32(state, spv::ScopeDevice), ConstantU32(state, spv::MemorySemanticsMaskNone), Binary(state, spv::OpBitwiseOr, u32, record, stage)); }); StopBdaInvocationIf(state, poisoned); } @@ -1900,6 +1903,10 @@ std::uint32_t EmitDataOrderedCount(SpirvValueEmitContext& ctx, const IrValue& in return AppendConsume(ctx, inst, true, true); } +std::uint32_t EmitDataOrderedSwap(SpirvValueEmitContext& ctx, const IrValue& inst) { + return AppendConsume(ctx, inst, true, true, true); +} + std::uint32_t EmitSharedAtomicRsub32(SpirvValueEmitContext& ctx, const IrValue& inst) { const auto data = ctx.Arg(inst, 1); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index bd58498bc8..5fad9ba460 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); } @@ -339,9 +341,12 @@ void DefineModule(SpirvEmitterState& state) { state.module.EmitCapability(spv::CapabilitySignedZeroInfNanPreserve); state.module.EmitExtension("SPV_KHR_float_controls"); state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeSignedZeroInfNanPreserve, 32u); - if (state.float32FlushToZero) { + if (state.float32FlushToZero && std::find(state.supportedCapabilities.begin(), state.supportedCapabilities.end(), static_cast(spv::CapabilityDenormFlushToZero)) != state.supportedCapabilities.end()) { state.module.EmitCapability(spv::CapabilityDenormFlushToZero); state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeDenormFlushToZero, 32u); + } else if (state.float32DenormPreserve && !state.float32FlushToZero) { + state.module.EmitCapability(spv::CapabilityDenormPreserve); + state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeDenormPreserve, 32u); } const bool deviceFloat64 = std::find(state.supportedCapabilities.begin(), state.supportedCapabilities.end(), static_cast(spv::CapabilityFloat64)) != state.supportedCapabilities.end(); state.integerInterpolation = state.requirements.float64Interpolation && !deviceFloat64; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 887337e6ea..8e8d4fe56e 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -27,6 +27,48 @@ bool UserDataDwordIndex(const SpirvEmitterState& state, ScalarReg reg, std::uint return true; } +bool MeshBarycentricPixel(const SpirvEmitterState& state) { + return state.meshBarycentricPixel && state.program.Metadata().barycentricEmulation && state.pixelFlatPositions != 0u; +} + +std::uint32_t EmitSolvedBarycentric(SpirvEmitterState& state, StageInputKind kind) { + const auto cached = state.solvedBarycentrics.find({state.currentLabel, static_cast(kind)}); + if (cached != state.solvedBarycentrics.end()) return cached->second; + const auto vec3 = TypeF32Vector(state, 3u); + const auto varying = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, vec3, varying, InputVariableForKind(state, kind)); + std::array corner {}; + for (std::uint32_t index = 0; index < 3u; index++) { + const auto pointer = state.module.AllocateId(); + const auto position = state.module.AllocateId(); + corner.at(index) = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassInput, TypeF32Vector(state, 4u)), pointer, state.pixelFlatPositions, ConstantU32(state, index)); + state.module.AddFunction(spv::OpLoad, TypeF32Vector(state, 4u), position, pointer); + state.module.AddFunction(spv::OpVectorShuffle, vec3, corner.at(index), position, position, 0u, 1u, 3u); + } + const auto cross = [&](std::uint32_t left, std::uint32_t right) { + const auto result = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, vec3, result, GlslStd450(state), GLSLstd450Cross, left, right); + return result; + }; + const auto dot = [&](std::uint32_t left, std::uint32_t right) { + const auto result = state.module.AllocateId(); + state.module.AddFunction(spv::OpDot, TypeF32(state), result, left, right); + return result; + }; + const auto determinant = dot(corner[0], cross(corner[1], corner[2])); + std::array weight {}; + const std::array, 3> rows{{{1u, 2u}, {2u, 0u}, {0u, 1u}}}; + for (std::uint32_t index = 0; index < 3u; index++) { + weight.at(index) = state.module.AllocateId(); + state.module.AddFunction(spv::OpFDiv, TypeF32(state), weight.at(index), dot(varying, cross(corner[rows[index][0]], corner[rows[index][1]])), determinant); + } + const auto solved = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, vec3, solved, weight[0], weight[1], weight[2]); + state.solvedBarycentrics[{state.currentLabel, static_cast(kind)}] = solved; + return solved; +} + const ShaderWorkgroupInputInfo* ShaderWorkgroupInputFor(const SpirvEmitterState& state) { switch (state.program.Resources().stage) { case IrShaderStage::Compute: @@ -38,6 +80,31 @@ const ShaderWorkgroupInputInfo* ShaderWorkgroupInputFor(const SpirvEmitterState& } } +namespace { + +std::uint32_t GuestSampleIndex(SpirvEmitterState& state, std::uint32_t hostIndex) { + if (state.sampleOrderWord == 0u) return hostIndex; + const auto shift = EmitBinaryU32(state, spv::OpShiftLeftLogical, EmitBinaryU32(state, spv::OpBitwiseAnd, hostIndex, ConstantU32(state, 7u)), ConstantU32(state, 2u)); + return EmitBinaryU32(state, spv::OpBitwiseAnd, EmitBinaryU32(state, spv::OpShiftRightLogical, state.sampleOrderWord, shift), ConstantU32(state, 7u)); +} + +std::uint32_t ReorderedSampleBits(SpirvEmitterState& state, std::uint32_t bits, bool toHost) { + auto reordered = EmitBinaryU32(state, spv::OpBitwiseAnd, bits, ConstantU32(state, ~0xffu)); + for (std::uint32_t sample = 0; sample < 8u; ++sample) { + const auto guest = EmitBinaryU32(state, spv::OpBitwiseAnd, EmitBinaryU32(state, spv::OpShiftRightLogical, state.sampleOrderWord, ConstantU32(state, sample * 4u)), ConstantU32(state, 7u)); + if (toHost) { + const auto bit = EmitBinaryU32(state, spv::OpBitwiseAnd, EmitBinaryU32(state, spv::OpShiftRightLogical, bits, guest), ConstantU32(state, 1u)); + reordered = EmitBinaryU32(state, spv::OpBitwiseOr, reordered, EmitBinaryU32(state, spv::OpShiftLeftLogical, bit, ConstantU32(state, sample))); + } else { + const auto bit = EmitBinaryU32(state, spv::OpBitwiseAnd, EmitBinaryU32(state, spv::OpShiftRightLogical, bits, ConstantU32(state, sample)), ConstantU32(state, 1u)); + reordered = EmitBinaryU32(state, spv::OpBitwiseOr, reordered, EmitBinaryU32(state, spv::OpShiftLeftLogical, bit, guest)); + } + } + return reordered; +} + +} + std::uint32_t EmitBuiltinU32(SpirvEmitterState& state, StageInputKind kind, std::uint32_t component); std::uint32_t EmitQuadPixel(SpirvEmitterState& state) { @@ -162,7 +229,7 @@ std::uint32_t EmitBuiltinU32(SpirvEmitterState& state, StageInputKind kind, std: const auto bits = state.module.AllocateId(); state.module.AddFunction(spv::OpLoad, TypeI32(state), value, variable); state.module.AddFunction(spv::OpBitcast, TypeU32(state), bits, value); - return bits; + return kind == StageInputKind::SampleId ? GuestSampleIndex(state, bits) : bits; } if (kind == StageInputKind::SampleMask) { const auto pointer = state.module.AllocateId(); @@ -182,6 +249,13 @@ std::uint32_t EmitBuiltinU32(SpirvEmitterState& state, StageInputKind kind, std: state.module.AddFunction(spv::OpBitcast, TypeU32(state), bits, value); return bits; } + if ((kind == StageInputKind::BaryCoordSmooth || kind == StageInputKind::BaryCoordNoPerspective) && MeshBarycentricPixel(state)) { + const auto value = state.module.AllocateId(); + const auto bits = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, TypeF32(state), value, EmitSolvedBarycentric(state, kind), component + 1u); + state.module.AddFunction(spv::OpBitcast, TypeU32(state), bits, value); + return bits; + } if (kind == StageInputKind::BaryCoordSmooth || kind == StageInputKind::BaryCoordNoPerspective) { const auto pointer = state.module.AllocateId(); const auto value = state.module.AllocateId(); @@ -228,12 +302,17 @@ std::uint32_t EmitAttributeValue(SpirvEmitterState& state, std::uint32_t attr, s throw std::runtime_error("per-vertex attribute interpolation lacks its barycentric input"); } std::uint32_t sum = 0; + const auto solved = MeshBarycentricPixel(state) ? EmitSolvedBarycentric(state, barycentricKind) : 0u; for (std::uint32_t vertex = 0; vertex < 3u; vertex++) { const auto pointer = state.module.AllocateId(); const auto weight = state.module.AllocateId(); const auto product = state.module.AllocateId(); - state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassInput, TypeF32(state)), pointer, barycentric, ConstantU32(state, vertex)); - state.module.AddFunction(spv::OpLoad, TypeF32(state), weight, pointer); + if (solved != 0u) { + state.module.AddFunction(spv::OpCompositeExtract, TypeF32(state), weight, solved, vertex); + } else { + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassInput, TypeF32(state)), pointer, barycentric, ConstantU32(state, vertex)); + state.module.AddFunction(spv::OpLoad, TypeF32(state), weight, pointer); + } state.module.AddFunction(spv::OpFMul, TypeF32(state), product, loadPerVertex(vertex), weight); if (vertex == 0u) { sum = product; @@ -364,6 +443,7 @@ bool AlphaToCoverage(const SpirvEmitterState& state) { void StoreCoverage(SpirvEmitterState& state, std::uint32_t mask) { if (state.quadSampleCoverage != 0u) mask = Binary(state, spv::OpBitwiseAnd, TypeI32(state), mask, state.quadSampleCoverage); + if (state.sampleOrderWord != 0u) mask = Unary(state, spv::OpBitcast, TypeI32(state), ReorderedSampleBits(state, Unary(state, spv::OpBitcast, TypeU32(state), mask), true)); const auto pointer = state.module.AllocateId(); state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassOutput, TypeI32(state)), pointer, state.sampleMaskVariable, ConstantU32(state, 0u)); state.module.AddFunction(spv::OpStore, pointer, mask); @@ -739,17 +819,19 @@ std::uint32_t TessellationPointer(SpirvValueEmitContext& ctx, const IrValue& ins const auto address = ctx.Arg(inst, 1); const bool patch = kind == TessellationAttribute::PatchOutput || kind == TessellationAttribute::PatchInput; const bool local = kind == TessellationAttribute::LocalOutput || kind == TessellationAttribute::ControlInput; - const auto stride = local ? state.program.Metadata().tessellationLocalStride : state.program.Metadata().tessellationControlStride; + const auto stride = local ? state.program.Metadata().tessellationLocalStride : kind == TessellationAttribute::ControlShared ? state.program.Metadata().tessellationSharedStride : state.program.Metadata().tessellationControlStride; const auto type = TypePointer(state, storage, TypeU32(state)); const auto inPatch = [&](std::uint32_t vertex) { - if (kind != TessellationAttribute::ControlInput) return vertex; + if (kind != TessellationAttribute::ControlInput && kind != TessellationAttribute::EvaluationInput) return vertex; + const auto& tess = state.inputInfo.vertex->tess; const auto clamped = state.module.AllocateId(); - state.module.AddFunction(spv::OpExtInst, TypeU32(state), clamped, GlslStd450(state), GLSLstd450UMin, vertex, ConstantU32(state, state.inputInfo.vertex->tess.inputControlPoints - 1u)); + state.module.AddFunction(spv::OpExtInst, TypeU32(state), clamped, GlslStd450(state), GLSLstd450UMin, vertex, ConstantU32(state, (kind == TessellationAttribute::ControlInput ? tess.inputControlPoints : tess.outputControlPoints) - 1u)); return clamped; }; + const bool sharedRead = kind == TessellationAttribute::ControlShared && inst.Opcode() == IrOpcode::GetTessellationAttribute; const auto* sum = inst.Argument(1); const auto* scaled = sum->Opcode() == IrOpcode::IAdd32 ? sum->Argument(0) : nullptr; - if (kind == TessellationAttribute::ControlInput && scaled != nullptr && sum->Argument(1)->HasImmediate() && scaled->Opcode() == IrOpcode::IMul32 && scaled->Argument(1)->HasImmediate() && scaled->Argument(1)->ImmediateU32() == stride) { + if ((kind == TessellationAttribute::ControlInput || sharedRead) && scaled != nullptr && sum->Argument(1)->HasImmediate() && scaled->Opcode() == IrOpcode::IMul32 && scaled->Argument(1)->HasImmediate() && scaled->Argument(1)->ImmediateU32() == stride) { const auto constant = sum->Argument(1)->ImmediateU32(); const auto vertex = inPatch(EmitBinaryU32(state, spv::OpIAdd, ctx.Arg(*scaled, 0), ConstantU32(state, constant / stride))); state.module.AddFunction(spv::OpAccessChain, type, pointer, variable, vertex, ConstantU32(state, constant % stride / 16u), ConstantU32(state, constant % stride % 16u / 4u)); @@ -761,7 +843,8 @@ std::uint32_t TessellationPointer(SpirvValueEmitContext& ctx, const IrValue& ins if (kind == TessellationAttribute::LocalOutput || patch) { state.module.AddFunction(spv::OpAccessChain, type, pointer, variable, attribute, component); } else { - const auto vertex = kind == TessellationAttribute::ControlOutput ? EmitLaneId(state) : inPatch(EmitBinaryU32(state, spv::OpUDiv, address, ConstantU32(state, stride))); + const bool own = kind == TessellationAttribute::ControlOutput || (kind == TessellationAttribute::ControlShared && !sharedRead); + const auto vertex = own ? EmitLaneId(state) : inPatch(EmitBinaryU32(state, spv::OpUDiv, address, ConstantU32(state, stride))); state.module.AddFunction(spv::OpAccessChain, type, pointer, variable, vertex, attribute, component); } return pointer; @@ -770,7 +853,7 @@ std::uint32_t TessellationPointer(SpirvValueEmitContext& ctx, const IrValue& ins } void DefineTessellationInterfaces(SpirvEmitterState& state) { - std::array used {}; + std::array used {}; for (const IrBlock* block : state.program.BlockOrder()) { for (const IrValue* inst : block->Instructions()) { if (inst->Opcode() != IrOpcode::GetTessellationAttribute && inst->Opcode() != IrOpcode::SetTessellationAttribute) { @@ -804,7 +887,7 @@ void DefineTessellationInterfaces(SpirvEmitterState& state) { type = array(TypeU32Vector(state, 4u), (state.program.Metadata().tessellationPatchStride + 15u) / 16u); } else { const bool local = kind == TessellationAttribute::LocalOutput || kind == TessellationAttribute::ControlInput; - const auto stride = local ? state.program.Metadata().tessellationLocalStride : state.program.Metadata().tessellationControlStride; + const auto stride = local ? state.program.Metadata().tessellationLocalStride : kind == TessellationAttribute::ControlShared ? state.program.Metadata().tessellationSharedStride : state.program.Metadata().tessellationControlStride; type = array(TypeU32Vector(state, 4u), (stride + 15u) / 16u); if (kind != TessellationAttribute::LocalOutput) { type = array(type, local ? tess.inputControlPoints : tess.outputControlPoints); @@ -820,7 +903,11 @@ void DefineTessellationInterfaces(SpirvEmitterState& state) { state.module.AddAnnotation(spv::OpDecorate, state.tessInnerVariable, spv::DecorationBuiltIn, spv::BuiltInTessLevelInner); state.module.AddAnnotation(spv::OpDecorate, state.tessInnerVariable, spv::DecorationPatch); } else { - state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationLocation, patch ? (state.program.Metadata().tessellationControlStride + 15u) / 16u : 0u); + const auto controlLocations = (state.program.Metadata().tessellationControlStride + 15u) / 16u; + const bool patchUsed = used[static_cast(TessellationAttribute::PatchOutput)] || used[static_cast(TessellationAttribute::PatchInput)]; + const auto patchLocations = patchUsed ? (state.program.Metadata().tessellationPatchStride + 15u) / 16u : 0u; + const auto location = patch ? controlLocations : kind == TessellationAttribute::ControlShared ? controlLocations + patchLocations : 0u; + state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationLocation, location); } if (kind == TessellationAttribute::Factor || patch) { state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationPatch); @@ -876,10 +963,13 @@ void DefineMeshOutputs(SpirvEmitterState& state) { throw std::runtime_error("unsupported mesh output kind"); } const auto type = MeshOutputType(state, output.kind); - const auto layerBlock = output.kind == StageOutputKind::Layer ? state.module.DecoratedType(spv::OpTypeStruct, {{spv::OpMemberDecorate, {0u, spv::DecorationBuiltIn, spv::BuiltInLayer}}, {spv::OpMemberDecorate, {0u, spv::DecorationPerPrimitiveEXT}}, {spv::OpDecorate, {spv::DecorationBlock}}}, type) : 0u; - output.variableId = MeshArray(state, spv::StorageClassOutput, output.kind == StageOutputKind::Layer ? layerBlock : type, output.kind == StageOutputKind::Layer ? mesh.maxPrimitives : mesh.maxVertices); const bool shared = output.kind == StageOutputKind::Layer; output.meshDataVariable = MeshArray(state, shared ? spv::StorageClassWorkgroup : spv::StorageClassPrivate, type, shared ? mesh.maxVertices : state.laneCount); + if (state.meshBarycentricVertex && output.kind == StageOutputKind::Parameter && std::ranges::find(state.meshExportedLocations, output.location) == state.meshExportedLocations.end()) { + continue; + } + const auto layerBlock = output.kind == StageOutputKind::Layer ? state.module.DecoratedType(spv::OpTypeStruct, {{spv::OpMemberDecorate, {0u, spv::DecorationBuiltIn, spv::BuiltInLayer}}, {spv::OpMemberDecorate, {0u, spv::DecorationPerPrimitiveEXT}}, {spv::OpDecorate, {spv::DecorationBlock}}}, type) : 0u; + output.variableId = MeshArray(state, spv::StorageClassOutput, output.kind == StageOutputKind::Layer ? layerBlock : type, output.kind == StageOutputKind::Layer ? mesh.maxPrimitives : mesh.maxVertices); state.interfaceVariables.push_back(output.variableId); state.module.AddName(output.variableId, output.debugName); if (output.kind == StageOutputKind::Parameter) { @@ -891,6 +981,46 @@ void DefineMeshOutputs(SpirvEmitterState& state) { state.module.AddAnnotation(spv::OpDecorate, output.variableId, spv::DecorationInvariant); } } + if (state.meshBarycentricVertex) { + if (std::ranges::none_of(state.outputs, [](const SpirvOutputBinding& output) { return output.kind == StageOutputKind::Position; })) { + throw std::runtime_error("barycentric emulation on the mesh path needs the mesh program to export a position"); + } + const auto vec3 = TypeF32Vector(state, 3u); + state.meshPositionStaging = MeshArray(state, spv::StorageClassWorkgroup, TypeF32Vector(state, 4u), mesh.maxVertices); + const auto defineClip = [&](std::uint32_t location, bool linear, const char* name) { + if (location == BarycentricEmulationLayout::NoLocation) return 0u; + const auto variable = MeshArray(state, spv::StorageClassOutput, vec3, mesh.maxVertices); + state.interfaceVariables.push_back(variable); + state.module.AddName(variable, name); + state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationLocation, location); + if (linear) state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationNoPerspective); + return variable; + }; + state.meshClipSmooth = defineClip(state.barycentricSmoothLocation, false, "meshBarycentricClipSmooth"); + state.meshClipLinear = defineClip(state.barycentricLinearLocation, true, "meshBarycentricClipLinear"); + if (state.barycentricPositionsLocation == BarycentricEmulationLayout::NoLocation) { + throw std::runtime_error("barycentric emulation on the mesh path has no location for the corner positions"); + } + const auto corners = state.module.Type(spv::OpTypeArray, TypeF32Vector(state, 4u), ConstantU32(state, 3u)); + state.meshFlatPositions = MeshArray(state, spv::StorageClassOutput, corners, mesh.maxPrimitives); + state.interfaceVariables.push_back(state.meshFlatPositions); + state.module.AddName(state.meshFlatPositions, "meshBarycentricPositions"); + state.module.AddAnnotation(spv::OpDecorate, state.meshFlatPositions, spv::DecorationLocation, state.barycentricPositionsLocation); + state.module.AddAnnotation(spv::OpDecorate, state.meshFlatPositions, spv::DecorationPerPrimitiveEXT); + for (const auto& [sourceLocation, relocated] : state.meshPerVertexLocations) { + const auto source = std::ranges::find_if(state.outputs, [sourceLocation = sourceLocation](const SpirvOutputBinding& output) { return output.kind == StageOutputKind::Parameter && output.location == sourceLocation; }); + if (source == state.outputs.end()) { + throw std::runtime_error("barycentric emulation on the mesh path: the mesh program does not export per-vertex parameter " + std::to_string(sourceLocation)); + } + const auto staging = MeshArray(state, spv::StorageClassWorkgroup, TypeF32Vector(state, 4u), mesh.maxVertices); + const auto perPrimitive = MeshArray(state, spv::StorageClassOutput, corners, mesh.maxPrimitives); + state.interfaceVariables.push_back(perPrimitive); + state.module.AddName(perPrimitive, "meshPerVertexParameter" + std::to_string(sourceLocation)); + state.module.AddAnnotation(spv::OpDecorate, perPrimitive, spv::DecorationLocation, relocated); + state.module.AddAnnotation(spv::OpDecorate, perPrimitive, spv::DecorationPerPrimitiveEXT); + state.meshVertexParameters.push_back({source->meshDataVariable, staging, perPrimitive}); + } + } state.meshAllocation = MeshArray(state, spv::StorageClassWorkgroup, TypeU32(state), 2u); state.meshPrimitiveData = MeshArray(state, spv::StorageClassPrivate, TypeU32(state), state.laneCount); state.meshPrimitives = MeshArray(state, spv::StorageClassOutput, TypeU32Vector(state, mesh.outputPrimitiveSize), mesh.maxPrimitives); @@ -967,14 +1097,15 @@ void EmitMeshEntryPoint(SpirvEmitterState& state) { const auto anyVertex = state.module.AllocateId(); state.module.AddFunction(spv::OpExtInst, TypeU32(state), anyVertex, GlslStd450(state), GLSLstd450UMax, vertices, ConstantU32(state, 1u)); const auto lastVertex = EmitBinaryU32(state, spv::OpISub, anyVertex, ConstantU32(state, 1u)); - for (std::uint32_t half = 0; half < state.laneCount; half++) { + const bool emulating = state.meshBarycentricVertex; + const auto emitVertexPart = [&](std::uint32_t half) { state.laneHalf = half; const auto index = EmitLocalInvocationIndex(state); const auto isVertex = state.module.AllocateId(); state.module.AddFunction(spv::OpULessThan, TypeBool(state), isVertex, index, vertices); EmitIfCondition(state, isVertex, [&]() { for (const auto& output : state.outputs) { - if (output.kind == StageOutputKind::Layer) { + if (output.kind == StageOutputKind::Layer || output.variableId == 0u) { continue; } const auto type = MeshOutputType(state, output.kind); @@ -982,7 +1113,28 @@ void EmitMeshEntryPoint(SpirvEmitterState& state) { const auto pointer = IsMeshDistance(output.kind) ? MeshDistancePointer(state, output, index) : MeshElement(state, output.variableId, spv::StorageClassOutput, type, index); state.module.AddFunction(spv::OpStore, pointer, value); } + if (!emulating) { + return; + } + const auto position = std::ranges::find_if(state.outputs, [](const SpirvOutputBinding& output) { return output.kind == StageOutputKind::Position; }); + const auto value = MeshLoad(state, position->meshDataVariable, spv::StorageClassPrivate, TypeF32Vector(state, 4u), ConstantU32(state, half)); + state.module.AddFunction(spv::OpStore, MeshElement(state, state.meshPositionStaging, spv::StorageClassWorkgroup, TypeF32Vector(state, 4u), index), value); + const auto clip = state.module.AllocateId(); + state.module.AddFunction(spv::OpVectorShuffle, TypeF32Vector(state, 3u), clip, value, value, 0u, 1u, 3u); + for (const auto& parameter : state.meshVertexParameters) { + const auto staged = MeshLoad(state, parameter[0], spv::StorageClassPrivate, TypeF32Vector(state, 4u), ConstantU32(state, half)); + state.module.AddFunction(spv::OpStore, MeshElement(state, parameter[1], spv::StorageClassWorkgroup, TypeF32Vector(state, 4u), index), staged); + } + for (const auto variable : {state.meshClipSmooth, state.meshClipLinear}) { + if (variable != 0u) { + state.module.AddFunction(spv::OpStore, MeshElement(state, variable, spv::StorageClassOutput, TypeF32Vector(state, 3u), index), clip); + } + } }); + }; + const auto emitPrimitivePart = [&](std::uint32_t half) { + state.laneHalf = half; + const auto index = EmitLocalInvocationIndex(state); const auto isPrimitive = state.module.AllocateId(); state.module.AddFunction(spv::OpULessThan, TypeBool(state), isPrimitive, index, primitives); EmitIfCondition(state, isPrimitive, [&]() { @@ -1011,7 +1163,41 @@ void EmitMeshEntryPoint(SpirvEmitterState& state) { state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassOutput, TypeU32(state)), pointer, output.variableId, index, ConstantU32(state, 0u)); state.module.AddFunction(spv::OpStore, pointer, layer); } + if (emulating) { + const auto corners = state.module.Type(spv::OpTypeArray, TypeF32Vector(state, 4u), ConstantU32(state, 3u)); + std::array staged {}; + for (std::uint32_t component = 0; component < 3u; component++) { + staged.at(component) = MeshLoad(state, state.meshPositionStaging, spv::StorageClassWorkgroup, TypeF32Vector(state, 4u), vertex.at(component)); + } + const auto array = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, corners, array, staged[0], staged[1], staged[2]); + state.module.AddFunction(spv::OpStore, MeshElement(state, state.meshFlatPositions, spv::StorageClassOutput, corners, index), array); + for (const auto& parameter : state.meshVertexParameters) { + std::array values {}; + for (std::uint32_t component = 0; component < 3u; component++) { + values.at(component) = MeshLoad(state, parameter[1], spv::StorageClassWorkgroup, TypeF32Vector(state, 4u), vertex.at(component)); + } + const auto perVertex = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, corners, perVertex, values[0], values[1], values[2]); + state.module.AddFunction(spv::OpStore, MeshElement(state, parameter[2], spv::StorageClassOutput, corners, index), perVertex); + } + } }); + }; + if (!emulating) { + for (std::uint32_t half = 0; half < state.laneCount; half++) { + emitVertexPart(half); + emitPrimitivePart(half); + } + } else { + for (std::uint32_t half = 0; half < state.laneCount; half++) { + emitVertexPart(half); + } + state.laneHalf = 0; + state.module.AddFunction(spv::OpControlBarrier, ConstantU32(state, spv::ScopeWorkgroup), ConstantU32(state, spv::ScopeWorkgroup), ConstantU32(state, spv::MemorySemanticsAcquireReleaseMask | spv::MemorySemanticsWorkgroupMemoryMask)); + for (std::uint32_t half = 0; half < state.laneCount; half++) { + emitPrimitivePart(half); + } } state.laneHalf = 0; state.module.AddFunction(spv::OpReturn); @@ -1347,6 +1533,18 @@ void EmitProgram(SpirvEmitterState& state) { if (state.pixelValidMaskVariable != 0u) { state.module.AddFunction(spv::OpStore, state.pixelValidMaskVariable, ConstantU32(state, 1u)); } + if (state.program.Resources().stage == IrShaderStage::Pixel && state.inputInfo.pixel != nullptr && state.inputInfo.pixel->ReordersSamples()) { + const auto& order = state.inputInfo.pixel->sampleOrder; + const auto parity = [&](std::uint32_t component) { + const auto value = Unary(state, spv::OpBitcast, TypeF32(state), EmitBuiltinU32(state, StageInputKind::FragCoord, component)); + return EmitBinaryU32(state, spv::OpBitwiseAnd, Unary(state, spv::OpConvertFToU, TypeU32(state), value), ConstantU32(state, 1u)); + }; + const auto quad = EmitBinaryU32(state, spv::OpBitwiseOr, parity(0u), EmitBinaryU32(state, spv::OpShiftLeftLogical, parity(1u), ConstantU32(state, 1u))); + const auto word = [&](std::size_t pixel) { return ConstantU32(state, order[pixel] == 0u ? 0x76543210u : order[pixel]); }; + auto selected = word(3u); + for (std::uint32_t pixel = 3u; pixel-- > 0u;) selected = Select(state, TypeU32(state), EmitCompareU32Constant(state, spv::OpIEqual, quad, pixel), word(pixel), selected); + state.sampleOrderWord = selected; + } if (AlphaToCoverage(state)) StoreCoverage(state, ConstantI32(state, -1)); EmitBdaAnchors(state); if (OrderedPixelShader(state)) { @@ -1368,7 +1566,9 @@ void EmitProgram(SpirvEmitterState& state) { state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassInput, TypeI32(state)), pointer, state.sampleMaskInputVariable, ConstantU32(state, 0u)); const auto rasterized = state.module.AllocateId(); state.module.AddFunction(spv::OpLoad, TypeI32(state), rasterized, pointer); - bit = EmitBinaryU32(state, spv::OpBitwiseAnd, Unary(state, spv::OpBitcast, TypeU32(state), rasterized), covered); + auto guestRasterized = Unary(state, spv::OpBitcast, TypeU32(state), rasterized); + if (state.sampleOrderWord != 0u) guestRasterized = ReorderedSampleBits(state, guestRasterized, false); + bit = EmitBinaryU32(state, spv::OpBitwiseAnd, guestRasterized, covered); state.quadSampleCoverage = Unary(state, spv::OpBitcast, TypeI32(state), covered); } const auto mask = static_cast(pixel.quadPixelMask); @@ -1652,7 +1852,8 @@ void EmitSetAttribute(SpirvValueEmitContext& ctx, const IrValue& inst) { state.module.AddFunction(spv::OpStore, state.depthVariable, f32); } if ((exp.en & 2u) != 0u && state.stencilRefVariable != 0u) { - const auto raw = ExportRawComponent(ctx, data, 1); + auto raw = ExportRawComponent(ctx, data, 1); + if (state.inputInfo.pixel != nullptr && state.inputInfo.pixel->psStencilOperationValueExport) raw = EmitBinaryU32(state, spv::OpBitwiseAnd, EmitBinaryU32(state, spv::OpShiftRightLogical, raw, ConstantU32(state, 8u)), ConstantU32(state, 0xffu)); const auto value = state.module.AllocateId(); state.module.AddFunction(spv::OpBitcast, TypeI32(state), value, raw); state.module.AddFunction(spv::OpStore, state.stencilRefVariable, value); 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/SpirvBackend/src/SpirvSpecialization.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp index cad0e161fd..d77a063d72 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp @@ -5,15 +5,67 @@ #include #include #include +#include #include #include #include +#include #include namespace ShaderRecompiler { namespace { -using Instruction = std::vector; +class WordArena { +public: + explicit WordArena(std::size_t words) : next(std::max(words, 1024u)) {} + + std::uint32_t* Allocate(std::size_t count) { + if (count > available) { + const auto size = std::max(count, next); + chunks.push_back(std::make_unique(size)); + cursor = chunks.back().get(); + available = size; + next = size * 2u; + } + auto* words = cursor; + cursor += count; + available -= count; + return words; + } + +private: + std::vector> chunks; + std::uint32_t* cursor = nullptr; + std::size_t available = 0; + std::size_t next; +}; + +template +struct ArenaAllocator { + using value_type = T; + + ArenaAllocator() = default; + explicit ArenaAllocator(WordArena* arena) : arena(arena) {} + template + ArenaAllocator(const ArenaAllocator& other) : arena(other.arena) {} + + T* allocate(std::size_t count) { + static_assert(std::is_same_v); + return arena != nullptr ? arena->Allocate(count) : std::allocator{}.allocate(count); + } + + void deallocate(T* words, std::size_t count) { + if (arena == nullptr) std::allocator{}.deallocate(words, count); + } + + friend bool operator==(const ArenaAllocator& left, const ArenaAllocator& right) { + return left.arena == right.arena; + } + + WordArena* arena = nullptr; +}; + +using Instruction = std::vector>; spv::Op Opcode(const Instruction& instruction) { return instruction.empty() ? spv::OpNop : static_cast(instruction[0] & 0xffffu); @@ -518,7 +570,7 @@ private: class Specialization { public: - explicit Specialization(std::span words) { + explicit Specialization(std::span words) : arena(words.size()) { if (words.size() < 5u || words[0] != spv::MagicNumber) throw std::runtime_error("invalid prepared SPIR-V header"); header.assign(words.begin(), words.begin() + 5); bound = header[3]; @@ -531,7 +583,7 @@ public: for (std::size_t cursor = 5; cursor < words.size();) { const auto count = words[cursor] >> 16u; if (count == 0u || count > words.size() - cursor) throw std::runtime_error("truncated prepared SPIR-V instruction"); - instructions.emplace_back(words.begin() + cursor, words.begin() + cursor + count); + instructions.emplace_back(words.begin() + cursor, words.begin() + cursor + count, ArenaAllocator(&arena)); const auto& instruction = instructions.back(); const auto op = Opcode(instruction); bool hasResult = false; @@ -1030,6 +1082,7 @@ private: } std::vector header; + WordArena arena; std::vector instructions; std::vector constants; std::uint32_t bound = 0; diff --git a/core/shader/recompiler/StencilReplay.cpp b/core/shader/recompiler/StencilReplay.cpp index f44d54f3d4..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 @@ -567,7 +568,7 @@ std::optional> Values(const IrProgram& program, const Ir if (value->Opcode() == IrOpcode::CompositeConstructU32x4 && *component < value->ArgumentCount()) return finish(child(value->Argument(*component))); return finish(std::nullopt); } - if (value->HasImmediate() && value->Type() == IrType::U32) return finish(std::set{value->ImmediateU32() & 0xffu}); + if (value->HasImmediate() && value->Type() == IrType::U32) return finish(std::set{value->ImmediateU32()}); if (value->Opcode() == IrOpcode::CompositeExtractU32x4 && value->ArgumentCount() == 2u && value->Argument(1)->Resolve()->HasImmediate()) return finish(child(value->Argument(0), value->Argument(1)->Resolve()->ImmediateU32())); if (value->Opcode() == IrOpcode::SelectU32 || value->Opcode() == IrOpcode::Select) { if (value->ArgumentCount() != 3u) return finish(std::nullopt); @@ -593,6 +594,10 @@ std::optional> Values(const IrProgram& program, const Ir } std::vector StencilReferenceValues(const IrProgram& program) { + return StencilReferenceValues(program, 0u); +} + +std::vector StencilReferenceValues(const IrProgram& program, std::uint32_t shift) { std::set result; for (const auto& block : program.Blocks()) { for (const auto* instruction : block->Instructions()) { @@ -601,9 +606,12 @@ std::vector StencilReferenceValues(const IrProgram& program) { 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 {}; - result.insert(values->begin(), values->end()); - if (result.size() > 16u) 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); } } return {result.begin(), result.end()}; diff --git a/core/shader/recompiler/StencilReplay.hpp b/core/shader/recompiler/StencilReplay.hpp index 2b45bf0cfd..5c998b0056 100644 --- a/core/shader/recompiler/StencilReplay.hpp +++ b/core/shader/recompiler/StencilReplay.hpp @@ -7,6 +7,7 @@ namespace ShaderRecompiler { [[nodiscard]] std::vector StencilReferenceValues(const IrProgram& program); +[[nodiscard]] std::vector StencilReferenceValues(const IrProgram& program, std::uint32_t shift); [[nodiscard]] bool StencilReplaySafe(const IrProgram& program); [[nodiscard]] bool StencilReferenceDefinedForKeptInvocations(const IrProgram& program); diff --git a/core/shader/recompiler/Translation/include/Translation/ShaderInputInfoBuilder.hpp b/core/shader/recompiler/Translation/include/Translation/ShaderInputInfoBuilder.hpp index 8fe4a01871..4f7c73ee5c 100644 --- a/core/shader/recompiler/Translation/include/Translation/ShaderInputInfoBuilder.hpp +++ b/core/shader/recompiler/Translation/include/Translation/ShaderInputInfoBuilder.hpp @@ -10,6 +10,7 @@ namespace ShaderRecompiler { // hostSubgroupSize is the device subgroup width; wave64 workgroups on 32-wide subgroups run each // invocation as two guest lanes so a guest wave stays one host subgroup. ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const GuestContext& context, std::uint32_t hostSubgroupSize, const MeshConfiguration* mesh = nullptr, const TessellationConfiguration* tessellation = nullptr); +void ValidateVertexStageInputs(ShaderStageKind stage, const GuestContext& context, const MeshConfiguration* mesh = nullptr, const TessellationConfiguration* tessellation = nullptr); } diff --git a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp index b2bf1d7b5e..a7748044c4 100644 --- a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp +++ b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp @@ -58,6 +58,7 @@ private: IrU1 threadBit(const std::array& mask); void writeRawU32(const RdnaOperand& operand, IrU32 value); IrF32 applyF32ResultModifiers(const RdnaOperand& operand, IrF32 value); + IrF32 scaleF32Result(std::uint32_t omod, IrF32 value); IrF32 applyF16ResultModifiers(const RdnaOperand& operand, IrF32 value); bool outputModifierApplies(std::uint32_t denormalShift) const; bool dx10Clamp() const { return !floatMode.has_value() || floatMode->dx10Clamp; } @@ -69,7 +70,7 @@ private: void writeOperand(const RdnaOperand& operand, IrValue* value); IrU32 packHalf2x16(IrF32 low, IrF32 high); void write16Bits(const RdnaOperand& operand, IrU32 value); - void writeF16(const RdnaOperand& operand, IrF32 value, std::initializer_list sources); + void writeF16(const RdnaOperand& operand, IrF32 value, std::initializer_list sources, IrValue* invalid = nullptr); bool directedF16Rounding() const; IrU32 flushF16Bits(IrU32 bits, std::uint32_t keepBit); IrU32 flushF16Input(IrU32 bits); @@ -86,10 +87,13 @@ private: IrU32 quietNan16(IrU32 bits); IrValue* nanResultF32(std::initializer_list sources, IrValue* result, IrValue* invalidProduct = nullptr); IrValue& invalidProductF32(IrValue* lhs, IrValue* rhs); + IrValue& invalidOperationF32(std::initializer_list sources, IrValue& invalidProduct); std::array quietNan64(const std::array& bits); std::array readU32Pair(const RdnaOperand& operand); IrU64 readU64(const RdnaOperand& operand); std::array readF64Bits(const RdnaOperand& operand); + std::array flushF64Bits(const std::array& bits, std::uint32_t keepBit); + IrValue& flushTinyF64Result(IrOpcode opcode, IrValue& lhs, IrValue& rhs, IrValue& result); IrU64 readF64(const RdnaOperand& operand); IrF32 readF16LaneAsF32(const RdnaOperand& operand, bool highLane, bool packed = false); IrF32 readF16AsF32(const RdnaOperand& operand); diff --git a/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp b/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp index 9c67377bf0..bb8094339a 100644 --- a/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp +++ b/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp @@ -28,8 +28,7 @@ void TranslationContext::sSubvectorLoop(const RdnaInstruction& inst, bool begin) IrU32 newCounter = counter; if (begin) { const IrU1 lowZero(ir.IEqual(execLo.Value(), zero.Value())); - const IrU1 highZero(ir.IEqual(execHi.Value(), zero.Value())); - const IrU1 execZero(ir.LogicalAnd(lowZero.Value(), highZero.Value())); + const IrU1 execZero(ir.IEqual(ir.Emit(IrOpcode::BitwiseOr32, IrType::U32, {&execLo.Value(), &execHi.Value()}), zero.Value())); const IrU32 entered(ir.Select(lowZero.Value(), execLo.Value(), execHi.Value())); newCounter = IrU32(ir.Select(execZero.Value(), counter.Value(), entered.Value())); newExecHi = IrU32(ir.Select(ir.LogicalOr(execZero.Value(), lowZero.Value()), execHi.Value(), zero.Value())); diff --git a/core/shader/recompiler/Translation/src/EmbeddedVertexFetch.cpp b/core/shader/recompiler/Translation/src/EmbeddedVertexFetch.cpp index 1e2e2d5fcc..966973b0f8 100644 --- a/core/shader/recompiler/Translation/src/EmbeddedVertexFetch.cpp +++ b/core/shader/recompiler/Translation/src/EmbeddedVertexFetch.cpp @@ -186,7 +186,17 @@ std::uint32_t embeddedFetchDstSize(const RdnaInstruction& inst) { std::uint32_t scalarOperandWidth(const RdnaInstruction& inst, std::uint32_t operandIndex) { switch (inst.family) { case RdnaInstructionFamily::SMEM: - return operandIndex == 0u ? 4u : 1u; + if (operandIndex != 0u) return 1u; + switch (inst.op) { + case RdnaOpcode::SLoadDword: + case RdnaOpcode::SLoadDwordx2: + case RdnaOpcode::SLoadDwordx4: + case RdnaOpcode::SLoadDwordx8: + case RdnaOpcode::SLoadDwordx16: + return 2u; + default: + return 4u; + } case RdnaInstructionFamily::MUBUF: case RdnaInstructionFamily::MTBUF: return operandIndex == 1u ? 4u : 1u; @@ -217,6 +227,39 @@ bool touchesSgpr(const RdnaInstruction& inst, std::uint32_t sgpr) { return false; } +bool killsSgpr(const RdnaInstruction& inst, std::uint32_t sgpr) { + if (!isScalarOperand(inst.destination)) return false; + const auto first = scalarSlot(inst.destination); + switch (inst.op) { + case RdnaOpcode::SCmovB32: + case RdnaOpcode::SCmovB64: + case RdnaOpcode::SBitset0B32: + case RdnaOpcode::SBitset0B64: + case RdnaOpcode::SBitset1B32: + case RdnaOpcode::SBitset1B64: + case RdnaOpcode::SMovreldB32: + case RdnaOpcode::SMovreldB64: + return false; + case RdnaOpcode::SMovkI32: + case RdnaOpcode::VReadlaneB32: + case RdnaOpcode::VReadfirstlaneB32: + return sgpr == first; + case RdnaOpcode::SLoadDword: + case RdnaOpcode::SLoadDwordx2: + case RdnaOpcode::SLoadDwordx4: + case RdnaOpcode::SLoadDwordx8: + case RdnaOpcode::SLoadDwordx16: + case RdnaOpcode::SBufferLoadDword: + case RdnaOpcode::SBufferLoadDwordx2: + case RdnaOpcode::SBufferLoadDwordx4: + case RdnaOpcode::SBufferLoadDwordx8: + case RdnaOpcode::SBufferLoadDwordx16: + return sgpr >= first && sgpr < first + decodedDstSize(inst); + default: + return (inst.family == RdnaInstructionFamily::SOP1 || inst.family == RdnaInstructionFamily::SOP2) && sgpr == first; + } +} + bool isVectorAlu(const RdnaInstruction& inst) { switch (inst.family) { case RdnaInstructionFamily::VOP1: @@ -350,6 +393,12 @@ bool writesExec(const RdnaInstruction& inst) { } } +bool setsExecAllOnes(const RdnaInstruction& inst) { + const bool allOnes = inst.source0.kind == RdnaOperandKind::IntegerInlineConstant && inst.source0.signedVal == -1; + if (!allOnes || inst.destination.kind != RdnaOperandKind::ExecLo) return false; + return inst.op == RdnaOpcode::SMovB64; +} + bool writesSgprMask(const RdnaInstruction& inst, std::uint32_t sgpr, std::uint32_t waveSize) { const auto covers = [&](const RdnaOperand& operand, std::uint32_t width) { return isScalarOperand(operand) && sgpr >= scalarSlot(operand) && sgpr < scalarSlot(operand) + width; }; return covers(inst.destination2, 2u) || (isVectorAlu(inst) && covers(inst.destination, waveSize == 64u ? 2u : 1u)); @@ -357,6 +406,8 @@ bool writesSgprMask(const RdnaInstruction& inst, std::uint32_t sgpr, std::uint32 bool carryObserved(const RdnaProgram& program, std::size_t addIndex, std::uint32_t waveSize) { const auto& add = program.instructions[addIndex]; + if (add.op == RdnaOpcode::VAddNcU32) return false; + if (add.op == RdnaOpcode::VSadU32) return false; if (add.destination2.kind == RdnaOperandKind::None || add.destination2.kind == RdnaOperandKind::Null) return false; if (!isScalarOperand(add.destination2)) return true; const auto first = scalarSlot(add.destination2); @@ -408,8 +459,9 @@ EmbeddedFetchPlan EmbeddedVertexFetchAnalyzer::Analyze(const RdnaProgram& progra const bool vertexIndexAccumulator = isVectorOperand(inst.destination) && (inst.destination.reg == 0u || (userDataBaseRegister == 8u && inst.destination.reg == kVertexIndexVgpr)); const bool instanceIndexAccumulator = isVectorOperand(inst.destination) && (inst.destination.reg == (userDataBaseRegister == 8u ? kInstanceIndexVgpr : 3u)); std::uint32_t sadZero = 0u; + const bool plainNoCarryAdd = inst.op == RdnaOpcode::VAddNcU32 && inst.family == RdnaInstructionFamily::VOP2 && inst.wordCount == 1u; const bool indexOffsetAdd = (vertexIndexAccumulator || instanceIndexAccumulator) && isScalarOperand(inst.source0) && - ((inst.op == RdnaOpcode::VAddI32 && isVectorOperand(inst.source1) && inst.source1.reg == inst.destination.reg) || + (((inst.op == RdnaOpcode::VAddI32 || plainNoCarryAdd) && isVectorOperand(inst.source1) && inst.source1.reg == inst.destination.reg) || (userDataBaseRegister == 8u && (inst.destination.reg == kVertexIndexVgpr || inst.destination.reg == kInstanceIndexVgpr) && inst.op == RdnaOpcode::VSadU32 && isVectorOperand(inst.source2) && inst.source2.reg == inst.destination.reg && tryConstantOperand(sgprs, inst.source1, sadZero) && sadZero == 0u)); @@ -604,9 +656,41 @@ EmbeddedFetchPlan EmbeddedVertexFetchAnalyzer::Analyze(const RdnaProgram& progra plan.instanceOffsetConflict = instanceOffsetConflict; const auto shared = [&](std::int32_t sgpr, const std::vector& addPcs) { if (sgpr < 0) return false; + const auto reg = static_cast(sgpr); + const auto isAdd = [&](const RdnaInstruction& inst) { return std::find(addPcs.begin(), addPcs.end(), inst.programCounter) != addPcs.end(); }; + const auto loopsBack = [&](std::size_t from, std::uint32_t pc) { + for (std::size_t i = from; i < program.instructions.size(); i++) { + const auto& inst = program.instructions[i]; + if (IsDirectBranchOpcode(inst.op) && inst.branchTarget <= pc) return true; + if (inst.op != RdnaOpcode::SEndpgm && !IsDirectBranchOpcode(inst.op) && leavesStraightLine(inst)) return true; + } + return false; + }; + std::uint32_t skippedUntil = 0u; + for (std::size_t i = 0; i < program.instructions.size(); i++) { + const auto& inst = program.instructions[i]; + if (isAdd(inst)) continue; + if (inst.op == RdnaOpcode::SMovrelsB32 || inst.op == RdnaOpcode::SMovrelsB64 || inst.op == RdnaOpcode::SMovrelsd2B32 || inst.op == RdnaOpcode::SMovreldB32 || inst.op == RdnaOpcode::SMovreldB64) return true; + if (readsSgprRange(inst, reg, 1u)) return true; + if (killsSgpr(inst, reg)) { + if (inst.programCounter < skippedUntil) continue; + return std::any_of(addPcs.begin(), addPcs.end(), [&](std::uint32_t pc) { return pc > inst.programCounter; }) || loopsBack(i + 1u, inst.programCounter); + } + if (touchesSgpr(inst, reg)) return true; + if (IsDirectBranchOpcode(inst.op)) { + if (inst.branchTarget <= inst.programCounter) break; + skippedUntil = std::max(skippedUntil, inst.branchTarget); + continue; + } + if (inst.op == RdnaOpcode::SEndpgm) { + if (inst.programCounter >= skippedUntil) return false; + continue; + } + if (leavesStraightLine(inst)) break; + } for (const auto& inst : program.instructions) { - if (std::find(addPcs.begin(), addPcs.end(), inst.programCounter) != addPcs.end()) continue; - if (touchesSgpr(inst, static_cast(sgpr))) return true; + if (isAdd(inst)) continue; + if (touchesSgpr(inst, reg)) return true; } return false; }; @@ -622,7 +706,7 @@ EmbeddedFetchPlan EmbeddedVertexFetchAnalyzer::Analyze(const RdnaProgram& progra if (add == program.instructions.end() || add->destination.reg != indexVgpr || add->destination.clamp || add->clampResult) return true; if (std::any_of(program.instructions.begin(), program.instructions.end(), [&](const RdnaInstruction& inst) { return inst.programCounter != add->programCounter && writesSgprMask(inst, static_cast(sgpr), waveSize); })) return true; const auto reg = add->destination.reg; - if (std::any_of(program.instructions.begin(), add, [&](const RdnaInstruction& inst) { return writesExec(inst) || mayReachVgpr(inst, reg); })) return true; + if (std::any_of(program.instructions.begin(), add, [&](const RdnaInstruction& inst) { return (writesExec(inst) && !setsExecAllOnes(inst)) || mayReachVgpr(inst, reg); })) return true; return carryObserved(program, static_cast(add - program.instructions.begin()), waveSize); }; plan.vertexIndexObserved = observed(plan.vertexOffsetSgpr, vertexAddPcs, kVertexIndexVgpr); diff --git a/core/shader/recompiler/Translation/src/Float64Instructions.cpp b/core/shader/recompiler/Translation/src/Float64Instructions.cpp index 14b1a3a554..1ae767d2af 100644 --- a/core/shader/recompiler/Translation/src/Float64Instructions.cpp +++ b/core/shader/recompiler/Translation/src/Float64Instructions.cpp @@ -10,18 +10,24 @@ IrU64 TranslationContext::readF64(const RdnaOperand& operand) { } bool TranslationContext::float64Operation(const RdnaInstruction& inst, IrOpcode opcode) { + const bool arithmetic = opcode == IrOpcode::FPAdd64 || opcode == IrOpcode::FPMul64; const std::size_t count = IrOpcodeOperandCount(opcode); std::array args{}; for (std::uint32_t index = 0u; index < count; ++index) { const RdnaOperand& operand = sourceAt(inst, index); switch (IrOpcodeArgumentType(opcode, index)) { - case IrType::U64: args[index] = &readF64(operand).Value(); break; + case IrType::U64: { + const std::array bits = opcode == IrOpcode::FPFrexpExp64 || arithmetic ? flushF64Bits(readF64Bits(operand), 0x40u) : readF64Bits(operand); + args[index] = &ir.ConstructU64(bits[0].Value(), bits[1].Value()); + break; + } case IrType::F32: args[index] = readOperand(operand, IrType::F32); break; default: args[index] = &readU32(operand).Value(); break; } } const IrType type = IrOpcodeType(opcode); IrValue* result = count == 1u ? &ir.Emit(opcode, type, {args[0]}) : count == 2u ? &ir.Emit(opcode, type, {args[0], args[1]}) : &ir.Emit(opcode, type, {args[0], args[1], args[2]}); + if (arithmetic) result = &flushTinyF64Result(opcode, *args[0], *args[1], *result); if (type == IrType::U64) { writeF64Result(inst.destination, *result); return true; @@ -33,6 +39,17 @@ bool TranslationContext::float64Operation(const RdnaInstruction& inst, IrOpcode return true; } +IrValue& TranslationContext::flushTinyF64Result(IrOpcode opcode, IrValue& lhs, IrValue& rhs, IrValue& result) { + if (!floatMode.has_value() || (floatMode->floatMode & 0x80u) != 0u) return result; + const bool product = opcode == IrOpcode::FPMul64; + IrValue& tested = product ? ir.Emit(IrOpcode::FPMul64, IrType::U64, {&ir.Emit(IrOpcode::FPMul64, IrType::U64, {&lhs, &ir.ConstantU64(0x43f0000000000000ull)}), &rhs}) : result; + IrValue& magnitude = ir.Emit(IrOpcode::BitwiseAnd64, IrType::U64, {&tested, &ir.ConstantU64(0x7fffffffffffffffull)}); + IrValue& below = ir.Emit(IrOpcode::ISub64, IrType::U64, {&magnitude, &ir.ConstantU64(1u)}); + IrValue& tiny = ir.Emit(IrOpcode::ULessThan64, IrType::U1, {&below, &ir.ConstantU64(product ? 0x040fffffffffffffull : 0x000fffffffffffffull)}); + IrValue& sign = ir.BitwiseAnd(ir.CompositeExtract(result, 1u), ir.Constant(0x80000000u)); + return ir.ConstructU64(ir.Select(tiny, ir.Constant(0u), ir.CompositeExtract(result, 0u)), ir.Select(tiny, sign, ir.CompositeExtract(result, 1u))); +} + bool TranslationContext::minMaxF64(const RdnaInstruction& inst, IrOpcode opcode) { std::array, 2> bits{readF64Bits(sourceAt(inst, 0u)), readF64Bits(sourceAt(inst, 1u))}; if (floatMode.has_value() && (floatMode->floatMode & 0x40u) == 0u) { @@ -63,14 +80,17 @@ bool TranslationContext::minMaxF64(const RdnaInstruction& inst, IrOpcode opcode) } bool TranslationContext::float64Unary(const RdnaInstruction& inst, IrOpcode opcode) { - const auto bits = readF64Bits(sourceAt(inst, 0u)); + const bool flushesInput = opcode == IrOpcode::FPFloor64 || opcode == IrOpcode::FPCeil64 || opcode == IrOpcode::FPFract64 || opcode == IrOpcode::FPFrexpMant64; + const auto bits = flushesInput ? flushF64Bits(readF64Bits(sourceAt(inst, 0u)), 0x40u) : readF64Bits(sourceAt(inst, 0u)); IrValue& argument = ir.ConstructU64(bits[0].Value(), bits[1].Value()); IrValue& result = opcode == IrOpcode::FPLdexp64 ? ir.Emit(opcode, IrType::U64, {&argument, &readU32(sourceAt(inst, 1u)).Value()}) : ir.Emit(opcode, IrType::U64, {&argument}); + std::array words{IrU32(ir.CompositeExtract(result, 0u)), IrU32(ir.CompositeExtract(result, 1u))}; + if (opcode == IrOpcode::FPFract64) words = flushF64Bits(words, 0x80u); const IrU32 magnitude(ir.BitwiseAnd(bits[1].Value(), ir.Constant(0x7fffffffu))); const IrU1 nan(ir.LogicalOr(ir.UGreaterThan(magnitude.Value(), ir.Constant(0x7ff00000u)), ir.LogicalAnd(ir.IEqual(magnitude.Value(), ir.Constant(0x7ff00000u)), ir.INotEqual(bits[0].Value(), ir.Constant(0u))))); const auto quiet = quietNan64(bits); - writeF64Result(inst.destination, ir.ConstructU64(ir.Select(nan.Value(), quiet[0].Value(), ir.CompositeExtract(result, 0u)), ir.Select(nan.Value(), quiet[1].Value(), ir.CompositeExtract(result, 1u)))); + writeF64Result(inst.destination, ir.ConstructU64(ir.Select(nan.Value(), quiet[0].Value(), words[0].Value()), ir.Select(nan.Value(), quiet[1].Value(), words[1].Value()))); return true; } diff --git a/core/shader/recompiler/Translation/src/FloatInstructions.cpp b/core/shader/recompiler/Translation/src/FloatInstructions.cpp index 6c3fcec7b5..0632342acb 100644 --- a/core/shader/recompiler/Translation/src/FloatInstructions.cpp +++ b/core/shader/recompiler/Translation/src/FloatInstructions.cpp @@ -282,7 +282,7 @@ bool TranslationContext::float16Ternary(const RdnaInstruction& inst, IrOpcode op if (opcode == IrOpcode::FPFma32 && !mix) { IrF32 result = fmaF16RoundedToOdd(IrF32(*args[0]), IrF32(*args[1]), IrF32(*args[2])); if (measured && (floatMode->floatMode & 0x80u) == 0u) result = flushTinyF16Value(result, 0x387ff000u); - writeF16(inst.destination, result, {args[0], args[1], args[2]}); + writeF16(inst.destination, result, {args[0], args[1], args[2]}, &invalidOperationF32({args[0], args[1], args[2]}, invalidProductF32(args[0], args[1]))); return true; } if (mix) { @@ -639,6 +639,12 @@ bool TranslationContext::floatTernary(const RdnaInstruction& inst, IrOpcode opco if (opcode == IrOpcode::FPFma32) result = &flushTinyProduct(args[0], args[1], result, args[2]).Value(); const bool f32Sources = !mix || !(sourceAt(inst, 0u).opSelHi || sourceAt(inst, 1u).opSelHi || (!accumulator && sourceAt(inst, 2u).opSelHi)); if (opcode == IrOpcode::FPFma32 && f32Sources) result = nanResultF32({args[0], args[1], args[2]}, result, &invalidProductF32(args[0], args[1])); + if (opcode == IrOpcode::FPFma32 && !f32Sources) { + IrValue& bits = ir.BitCastU32(*result); + IrValue& nan = ir.UGreaterThan(ir.BitwiseAnd(bits, ir.Constant(0x7fffffffu)), ir.Constant(0x7f800000u)); + IrValue& invalid = invalidOperationF32({args[0], args[1], args[2]}, invalidProductF32(args[0], args[1])); + result = &ir.BitCastF32(ir.Select(ir.LogicalAnd(nan, invalid), ir.Constant(0xffc00000u), bits)); + } writeOperand(inst.destination, result); return true; } @@ -680,10 +686,7 @@ IrU32 TranslationContext::minMaxF16Bits(bool maxValue, IrU32 lhs, IrU32 rhs) { return ir.BitwiseXor(value, ir.BitwiseAnd(ir.ShiftRightArithmetic(value, ir.Constant(31u)), ir.Constant(0x7fffu))); }; const IrU1 ordered(ir.Emit(maxValue ? IrOpcode::SGreaterThanEqual32 : IrOpcode::SLessThan32, IrType::U1, {&order(lhs), &order(rhs)})); - const IrU32 either(ir.BitwiseOr(lhs.Value(), rhs.Value())); - const IrU1 bothZero(ir.IEqual(ir.BitwiseAnd(either.Value(), ir.Constant(0x7fffu)), ir.Constant(0u))); - const IrU32 zero = maxValue ? IrU32(ir.BitwiseAnd(lhs.Value(), rhs.Value())) : either; - return IrU32(ir.Select(bothZero.Value(), zero.Value(), ir.Select(ordered.Value(), lhs.Value(), rhs.Value()))); + return IrU32(ir.Select(ordered.Value(), lhs.Value(), rhs.Value())); } bool TranslationContext::minMaxF16(const RdnaInstruction& inst, IrOpcode opcode) { @@ -938,7 +941,13 @@ bool TranslationContext::floatCube(const RdnaInstruction& inst, std::uint32_t re } const IrU32 bits(ir.BitCastU32(result.Value())); const IrU1 nan(ir.UGreaterThan(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffffffu)), ir.Constant(0x7f800000u))); - writeOperand(inst.destination, &ir.BitCastF32(ir.Select(nan.Value(), quietNan32(bits).Value(), bits.Value()))); + result = IrF32(ir.BitCastF32(ir.Select(nan.Value(), quietNan32(bits).Value(), bits.Value()))); + RdnaOperand destination = inst.destination; + if (destination.omod != 0u && !ieeeMode) { + result = scaleF32Result(destination.omod, result); + } + destination.omod = 0u; + writeOperand(destination, &result.Value()); return true; } diff --git a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp index e9f11b2e9b..cbd1075512 100644 --- a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp +++ b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp @@ -188,6 +188,10 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO const auto tessellationBase = [&entryIr](TessellationBaseKind kind) -> IrValue& { return entryIr.Emit(IrOpcode::TessellationBase, IrOpcodeType(IrOpcode::TessellationBase), {&entryIr.Constant(static_cast(kind))}); }; + constexpr std::uint32_t MergedHullRecordBytes = 16u; + const auto mergedHullRecord = [&entryIr](IrValue& address) -> IrValue& { + return entryIr.Emit(IrOpcode::GetTessellationAttribute, IrOpcodeType(IrOpcode::GetTessellationAttribute), {&entryIr.Constant(static_cast(TessellationAttribute::ControlInput)), &address, &entryIr.ConstantBool(true)}); + }; for (std::uint32_t index = 0; index < options.userDataCount; index++) { const auto reg = static_cast(options.userDataBaseRegister + index); @@ -325,6 +329,13 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO setVector(static_cast(2), drawIndex(StageInputKind::VertexIndex)); setVector(static_cast(3), tessellationBase(TessellationBaseKind::RelativeIndex)); setVector(static_cast(5), drawIndex(StageInputKind::InstanceIndex)); + if (options.inputInfo.vertex->tess.mergedHull) { + const auto kind = static_cast(TessellationAttribute::LocalOutput); + for (const auto [offset, index] : {std::pair{0u, StageInputKind::VertexIndex}, std::pair{4u, StageInputKind::InstanceIndex}}) { + static_cast(entryIr.Emit(IrOpcode::SetTessellationAttribute, IrType::Void, {&entryIr.Constant(kind), &entryIr.Constant(offset), &builtin(index), &entryIr.ConstantBool(true)})); + } + program.Metadata().tessellationLocalStride = MergedHullRecordBytes; + } } else if (options.stage == ShaderStageKind::TessellationControl) { const auto& tess = options.inputInfo.vertex->tess; entryIr.SetScalarReg(static_cast(2), tessellationBase(TessellationBaseKind::OffChip)); @@ -332,6 +343,14 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO entryIr.SetScalarReg(static_cast(3), entryIr.Constant(0x81010000u | tess.inputControlPoints | (tess.outputControlPoints << 8u))); setVector(static_cast(0), builtin(StageInputKind::PrimitiveId)); setVector(static_cast(1), entryIr.BitwiseOr(entryIr.ShiftLeftLogical(builtin(StageInputKind::InvocationId), entryIr.Constant(8u)), tessellationBase(TessellationBaseKind::RelativeIndex))); + if (tess.mergedHull) { + IrValue& point = entryIr.Emit(IrOpcode::UMin32, IrOpcodeType(IrOpcode::UMin32), {&builtin(StageInputKind::InvocationId), &entryIr.Constant(tess.inputControlPoints - 1u)}); + IrValue& record = entryIr.IMul(point, entryIr.Constant(MergedHullRecordBytes)); + setVector(static_cast(2), mergedHullRecord(record)); + setVector(static_cast(3), entryIr.IAdd(entryIr.IMul(tessellationBase(TessellationBaseKind::RelativeIndex), entryIr.Constant(tess.inputControlPoints)), builtin(StageInputKind::InvocationId))); + setVector(static_cast(5), mergedHullRecord(entryIr.IAdd(record, entryIr.Constant(4u)))); + program.Metadata().tessellationLocalStride = MergedHullRecordBytes; + } } else if (options.stage == ShaderStageKind::TessellationEvaluation) { entryIr.SetScalarReg(static_cast(3), entryIr.Constant(64u)); entryIr.SetScalarReg(static_cast(4), tessellationBase(TessellationBaseKind::OffChip)); @@ -436,6 +455,9 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO IrProgram InstructionTranslator::Translate(const RdnaProgram& decoded, const ControlFlowGraph& cfg, const TranslateOptions& options) const { validateTranslateOptions(options); + if (cfg.linkEscapeWaveSize == options.waveSize) { + throw std::invalid_argument(cfg.linkEscapeReason); + } if (cfg.blocks.empty()) { throw std::runtime_error("cannot translate an empty control flow graph"); } diff --git a/core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp b/core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp index b78f01f6d2..a781713ddc 100644 --- a/core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp +++ b/core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp @@ -299,7 +299,6 @@ bool TranslationContext::dsOrderedCount(const RdnaInstruction& inst) { const std::uint32_t flags = (inst.memoryOffset >> 8u) & 0xffu; const auto location = "ds_ordered_count at pc " + std::to_string(inst.programCounter); if (!inst.gds) throw std::runtime_error(location + " without GDS is not modeled"); - if ((flags & 0x10u) != 0u) throw std::runtime_error(location + ": ordered swap is not modeled"); if (((flags >> 6u) & 3u) != 0u) throw std::runtime_error(location + ": counting more than one dword is not modeled"); MemoryInfo memory = sharedMemoryInfoFromInstruction(inst); memory.offset = inst.memoryOffset & 0xffu; @@ -308,7 +307,8 @@ bool TranslationContext::dsOrderedCount(const RdnaInstruction& inst) { IrValue& active = ir.GetExec(); IrValue& activeLo = ir.GetExecLo(); IrValue& activeHi = ir.GetExecHi(); - IrValue& result = ir.Emit(IrOpcode::DataOrderedCount, IrOpcodeType(IrOpcode::DataOrderedCount), {&address, &active, &activeLo, &activeHi, &amount.Value()}, addMemoryInfo(memory, inst.programCounter)); + const auto opcode = (flags & 0x10u) != 0u ? IrOpcode::DataOrderedSwap : IrOpcode::DataOrderedCount; + IrValue& result = ir.Emit(opcode, IrOpcodeType(opcode), {&address, &active, &activeLo, &activeHi, &amount.Value()}, addMemoryInfo(memory, inst.programCounter)); writeOperand(inst.destination, &result); return true; } diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index 2d1b0ffc3f..88f4b0fad4 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -61,6 +61,18 @@ void _detectVertexBuffers(ShaderVertexInputInfo& info) { } } +void _validateTessellation(const TessellationConfiguration* tessellation) { + if (tessellation == nullptr) throw std::runtime_error("ShaderInputInfoBuilder: tessellation configuration is missing"); + if (tessellation->inputControlPoints == 0u || tessellation->inputControlPoints > 32u || tessellation->outputControlPoints == 0u || tessellation->outputControlPoints > 32u) throw std::runtime_error("ShaderInputInfoBuilder: invalid tessellation control-point counts"); + if ((tessellation->domain != 1u && tessellation->domain != 2u) || (tessellation->partitioning != 0u && tessellation->partitioning != 2u && tessellation->partitioning != 3u) || (tessellation->outputTopology != 2u && tessellation->outputTopology != 3u)) throw std::runtime_error("ShaderInputInfoBuilder: unsupported tessellation configuration"); + if (tessellation->patchesPerGroup == 0u || tessellation->patchesPerGroup > 256u) throw std::runtime_error("ShaderInputInfoBuilder: invalid tessellation patches per group"); +} + +void _validateMesh(const MeshConfiguration* mesh) { + if (mesh == nullptr) throw std::runtime_error("ShaderInputInfoBuilder: a mesh-stage program has no mesh configuration"); + if (mesh->threadsPerGroup == 0u || mesh->maxVertices == 0u || mesh->maxPrimitives == 0u || (mesh->outputPrimitiveSize != 2u && mesh->outputPrimitiveSize != 3u) || mesh->provokingVertex >= mesh->outputPrimitiveSize) throw std::runtime_error("ShaderInputInfoBuilder: invalid mesh configuration"); +} + } ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const GuestContext& context, std::uint32_t hostSubgroupSize, const MeshConfiguration* mesh, const TessellationConfiguration* tessellation) { @@ -141,6 +153,7 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues pixelStorage.psDepthExportEnable = pixel.depthExportEnable; pixelStorage.psSampleMaskExportEnable = pixel.sampleMaskExportEnable; pixelStorage.psStencilRefExportEnable = pixel.stencilRefExportEnable; + pixelStorage.psStencilOperationValueExport = pixel.stencilOperationValueExport; pixelStorage.psSampleShading = pixel.sampleShading; pixelStorage.psEarlyZ = pixel.earlyZ; pixelStorage.psExecuteOnNoop = pixel.executeOnNoop; @@ -154,6 +167,7 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues pixelStorage.psIteratesSamples = pixel.iteratesSamples; pixelStorage.psPositionLocation = pixel.positionLocation; pixelStorage.sampleLocations = pixel.sampleLocations; + pixelStorage.sampleOrder = pixel.sampleOrder; ShaderStageInputInfo result; result.pixel = &pixelStorage; return result; @@ -190,10 +204,7 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues } _detectVertexBuffers(vertexStorage); if (stage == ShaderStageKind::Local || stage == ShaderStageKind::TessellationControl || stage == ShaderStageKind::TessellationEvaluation) { - if (tessellation == nullptr) throw std::runtime_error("ShaderInputInfoBuilder: tessellation configuration is missing"); - if (tessellation->inputControlPoints == 0u || tessellation->inputControlPoints > 32u || tessellation->outputControlPoints == 0u || tessellation->outputControlPoints > 32u) throw std::runtime_error("ShaderInputInfoBuilder: invalid tessellation control-point counts"); - if ((tessellation->domain != 1u && tessellation->domain != 2u) || (tessellation->partitioning != 0u && tessellation->partitioning != 2u && tessellation->partitioning != 3u) || (tessellation->outputTopology != 2u && tessellation->outputTopology != 3u)) throw std::runtime_error("ShaderInputInfoBuilder: unsupported tessellation configuration"); - if (tessellation->patchesPerGroup == 0u || tessellation->patchesPerGroup > 256u) throw std::runtime_error("ShaderInputInfoBuilder: invalid tessellation patches per group"); + _validateTessellation(tessellation); auto& target = vertexStorage.tess; target.inputControlPoints = tessellation->inputControlPoints; target.outputControlPoints = tessellation->outputControlPoints; @@ -205,9 +216,10 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues target.breakAtInstance = tessellation->breakAtInstance; target.instancePatches = tessellation->instancePatches; target.drawParameters = tessellation->drawParameters; + target.mergedHull = tessellation->mergedHull; } if (stage == ShaderStageKind::Mesh) { - if (mesh == nullptr) throw std::runtime_error("ShaderInputInfoBuilder: a mesh-stage program has no mesh configuration"); + _validateMesh(mesh); auto& target = vertexStorage.mesh; target.threadsNum[0] = mesh->threadsPerGroup; target.threadsNum[1] = 1u; @@ -223,7 +235,6 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues target.provokingVertex = mesh->provokingVertex; target.esgsItemSize = mesh->esgsItemSize; target.outputPrimitiveSize = mesh->outputPrimitiveSize; - if (target.threadsNum[0] == 0u || target.maxVertices == 0u || target.maxPrimitives == 0u || (target.outputPrimitiveSize != 2u && target.outputPrimitiveSize != 3u) || target.provokingVertex >= target.outputPrimitiveSize) throw std::runtime_error("ShaderInputInfoBuilder: invalid mesh configuration"); } ShaderStageInputInfo result; result.vertex = &vertexStorage; @@ -236,4 +247,51 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues throw std::runtime_error("ShaderInputInfoBuilder: unexpected stage"); } +void ValidateVertexStageInputs(ShaderStageKind stage, const GuestContext& context, const MeshConfiguration* mesh, const TessellationConfiguration* tessellation) { + if (!context.vertex.has_value()) { + throw std::runtime_error("ShaderInputInfoBuilder: GuestContext.vertex is not set"); + } + const auto& vertex = *context.vertex; + if (vertex.resourcesNum > vertex.resources.size()) throw std::runtime_error("ShaderInputInfoBuilder: invalid vertex resource count"); + struct Buffer { + std::uint64_t addr; + std::uint32_t stride; + std::uint32_t numRecords; + std::uint32_t fetchIndex; + int attrNum; + }; + std::array buffers; + int buffersNum = 0; + const std::array embedded{}; + for (std::uint32_t ri = 0; ri < vertex.resourcesNum; ++ri) { + const auto& fields = vertex.fetchEmbedded ? embedded : vertex.resources[ri].fields; + const std::uint16_t stride = static_cast((fields[1] >> 16u) & 0x3fffu); + const std::uint64_t base = (static_cast(fields[0]) | (static_cast(fields[1]) << 32u)) & 0xffffffffffffull; + const std::uint32_t numRecords = fields[2]; + const std::uint32_t fetchIndex = vertex.fetchEmbedded ? 0u : vertex.resourcesDst[ri].fetchIndex; + bool merged = false; + for (int bi = 0; bi < buffersNum; ++bi) { + auto& b = buffers[bi]; + if (b.stride != stride || b.fetchIndex != fetchIndex) continue; + const auto low = base < b.addr ? base : b.addr; + if (base - low >= stride || b.addr - low >= stride) continue; + if (b.numRecords != numRecords) throw std::runtime_error("ShaderInputInfoBuilder: merged vertex buffers disagree on record count"); + b.addr = low; + if (b.attrNum >= ShaderVertexInputBuffer::MaxAttributes) throw std::runtime_error("ShaderInputInfoBuilder: vertex buffer attribute count exceeds the supported domain"); + ++b.attrNum; + merged = true; + break; + } + if (merged) continue; + if (buffersNum >= ShaderVertexInputInfo::MaxResources) throw std::runtime_error("ShaderInputInfoBuilder: vertex buffer count exceeds the supported domain"); + buffers[buffersNum++] = {base, stride, numRecords, fetchIndex, 1}; + } + if (stage == ShaderStageKind::Local || stage == ShaderStageKind::TessellationControl || stage == ShaderStageKind::TessellationEvaluation) { + _validateTessellation(tessellation); + } + if (stage == ShaderStageKind::Mesh) { + _validateMesh(mesh); + } +} + } diff --git a/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp b/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp index f82197547f..d54715a372 100644 --- a/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp +++ b/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp @@ -362,6 +362,15 @@ IrValue& TranslationContext::invalidProductF32(IrValue* lhs, IrValue* rhs) { return ir.LogicalOr(infZero(lhsMagnitude, rhsMagnitude), infZero(rhsMagnitude, lhsMagnitude)); } +IrValue& TranslationContext::invalidOperationF32(std::initializer_list sources, IrValue& invalidProduct) { + IrValue* noNan = &ir.ConstantBool(true); + for (IrValue* source : sources) { + IrValue& nan = ir.UGreaterThan(ir.BitwiseAnd(ir.BitCastU32(*source), ir.Constant(0x7fffffffu)), ir.Constant(0x7f800000u)); + noNan = &ir.LogicalAnd(*noNan, ir.LogicalNot(nan)); + } + return ir.LogicalOr(invalidProduct, *noNan); +} + IrU32 TranslationContext::quietNan16(IrU32 bits) { return ieeeMode ? IrU32(ir.BitwiseOr(bits.Value(), ir.Constant(0x0200u))) : bits; } @@ -370,21 +379,25 @@ std::array TranslationContext::quietNan64(const std::array& return ieeeMode ? std::array{bits[0], IrU32(ir.BitwiseOr(bits[1].Value(), ir.Constant(0x00080000u)))} : bits; } +IrF32 TranslationContext::scaleF32Result(std::uint32_t omod, IrF32 value) { + IrValue& bits = ir.BitCastU32(value.Value()); + IrValue& magnitude = ir.BitwiseAnd(bits, ir.Constant(0x7fffffffu)); + IrValue& sign = ir.BitwiseAnd(bits, ir.Constant(0x80000000u)); + IrValue* result = nullptr; + if (omod == 3u) { + result = &ir.Select(ir.ULessThan(magnitude, ir.Constant(0x01000000u)), sign, ir.ISub(bits, ir.Constant(0x00800000u))); + } else { + const std::uint32_t exponentStep = omod == 1u ? 0x00800000u : 0x01000000u; + result = &ir.Select(ir.ULessThan(magnitude, ir.Constant(0x7f800000u - exponentStep)), ir.IAdd(bits, ir.Constant(exponentStep)), ir.BitwiseOr(sign, ir.Constant(0x7f800000u))); + } + result = &ir.Select(ir.ULessThan(magnitude, ir.Constant(0x00800000u)), ir.Constant(0u), *result); + result = &ir.Select(ir.UGreaterThan(magnitude, ir.Constant(0x7f7fffffu)), bits, *result); + return IrF32(ir.BitCastF32(*result)); +} + IrF32 TranslationContext::applyF32ResultModifiers(const RdnaOperand& operand, IrF32 value) { if (operand.omod != 0u && outputModifierApplies(4u)) { - IrValue& bits = ir.BitCastU32(value.Value()); - IrValue& magnitude = ir.BitwiseAnd(bits, ir.Constant(0x7fffffffu)); - IrValue& sign = ir.BitwiseAnd(bits, ir.Constant(0x80000000u)); - IrValue* result = nullptr; - if (operand.omod == 3u) { - result = &ir.Select(ir.ULessThan(magnitude, ir.Constant(0x01000000u)), sign, ir.ISub(bits, ir.Constant(0x00800000u))); - } else { - const std::uint32_t exponentStep = operand.omod == 1u ? 0x00800000u : 0x01000000u; - result = &ir.Select(ir.ULessThan(magnitude, ir.Constant(0x7f800000u - exponentStep)), ir.IAdd(bits, ir.Constant(exponentStep)), ir.BitwiseOr(sign, ir.Constant(0x7f800000u))); - } - result = &ir.Select(ir.ULessThan(magnitude, ir.Constant(0x00800000u)), ir.Constant(0u), *result); - result = &ir.Select(ir.UGreaterThan(magnitude, ir.Constant(0x7f7fffffu)), bits, *result); - value = IrF32(ir.BitCastF32(*result)); + value = scaleF32Result(operand.omod, value); } if (operand.clamp) { const IrF32 saturated(ir.Emit(IrOpcode::FPSaturate32, IrType::F32, {&value.Value()})); diff --git a/core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp b/core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp index a199f1ff65..3ba6021ff2 100644 --- a/core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp +++ b/core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp @@ -52,6 +52,12 @@ std::array TranslationContext::readF64Bits(const RdnaOperand& operand) return bits; } +std::array TranslationContext::flushF64Bits(const std::array& bits, std::uint32_t keepBit) { + if (!floatMode.has_value() || (floatMode->floatMode & keepBit) != 0u) return bits; + const IrU1 tiny(ir.IEqual(ir.BitwiseAnd(bits[1].Value(), ir.Constant(0x7ff00000u)), ir.Constant(0u))); + return {IrU32(ir.Select(tiny.Value(), ir.Constant(0u), bits[0].Value())), IrU32(ir.Select(tiny.Value(), ir.BitwiseAnd(bits[1].Value(), ir.Constant(0x80000000u)), bits[1].Value()))}; +} + std::array TranslationContext::extractU64(IrU64 value) { return {IrU32(ir.CompositeExtract(value.Value(), 0u)), IrU32(ir.CompositeExtract(value.Value(), 1u))}; } @@ -248,10 +254,15 @@ void TranslationContext::writeF16Conversion(const RdnaOperand& operand, IrF32 va write16Bits(operand, flushTinyF16Conversion(modified, convertF32ToF16Bits(modified, sources))); } -void TranslationContext::writeF16(const RdnaOperand& operand, IrF32 value, std::initializer_list sources) { +void TranslationContext::writeF16(const RdnaOperand& operand, IrF32 value, std::initializer_list sources, IrValue* invalid) { const IrF16 half(ir.Emit(IrOpcode::ConvertF16F32, IrType::F16, {&applyF16ResultModifiers(operand, clampF16Overflow(value, sources)).Value()})); const IrU16 bits(ir.Emit(IrOpcode::BitCastU16F16, IrType::U16, {&half.Value()})); - write16Bits(operand, IrU32(ir.Emit(IrOpcode::ConvertU32U16, IrType::U32, {&bits.Value()}))); + IrU32 word(ir.Emit(IrOpcode::ConvertU32U16, IrType::U32, {&bits.Value()})); + if (invalid != nullptr) { + const IrU1 nan(ir.UGreaterThan(ir.BitwiseAnd(word.Value(), ir.Constant(0x7fffu)), ir.Constant(0x7c00u))); + word = IrU32(ir.Select(ir.LogicalAnd(nan.Value(), *invalid), ir.Constant(0xfe00u), word.Value())); + } + write16Bits(operand, word); } } 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/core/shader/recompiler/tests/RequestSerializerTests.cpp b/core/shader/recompiler/tests/RequestSerializerTests.cpp index dec5c2c74a..2696063735 100644 --- a/core/shader/recompiler/tests/RequestSerializerTests.cpp +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -45,9 +45,16 @@ void Require(bool condition) { if (!condition) std::abort(); } -void InsertPixelDefaultsAfterFrontFace(std::vector& bytes) { +void InsertMeshBarycentricFlag(std::vector& bytes) { + constexpr std::array draw{0x00, 0x34, 0xef, 0xcd, 0xab, 0x00, 0x00, 0x00, 57u, 0u, 0u, 0u, 4u, 0u, 0u, 0u, 9u, 0u, 0u, 0u}; + const auto at = std::search(bytes.begin(), bytes.end(), draw.begin(), draw.end()); + Require(at != bytes.end()); + bytes.insert(at + static_cast(draw.size()), 0u); +} + +void InsertPixelDefaultsAfterFrontFace(std::vector& bytes, bool sampleOrder = true) { bytes.insert(bytes.begin() + 261u, {0x0fu, 0xffu, 0xffu, 0u, 0u, 0xffu, 0xffu, 0u, 0u, 0xffu, 0xffu, 0u, 0u, 0xffu, 0xffu, 0u, 0u, 0u, 0u, 0u, 0u}); - bytes.insert(bytes.begin() + 282u, 34u, 0u); + bytes.insert(bytes.begin() + 282u, sampleOrder ? 51u : 35u, 0u); } std::vector DecodeBase64(std::string_view text) { @@ -207,11 +214,12 @@ int main() { Require(request.graphics->draw.indexAddress == 0xabcdef3400u && request.graphics->draw.indexCount == 57u && request.graphics->draw.indexElementBytes == 4u && request.graphics->draw.instanceCount == 9u); Require(request.target.float16DenormPreserve == preserve); auto upgraded = bytes; - upgraded[4] = 36u; + upgraded[4] = 42u; + InsertMeshBarycentricFlag(upgraded); if (fixture.pixel) upgraded.insert(upgraded.begin() + 258u, {0u, 0u, 0u}); if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(upgraded); - if (fixture.tessellation) upgraded.insert(upgraded.end() - 35u, 5u, 0u); - upgraded.insert(upgraded.end(), sizeof(std::uint64_t) + 1u + 5u + 1u, 0u); + if (fixture.tessellation) upgraded.insert(upgraded.end() - 35u, 6u, 0u); + upgraded.insert(upgraded.end(), sizeof(std::uint64_t) + 1u + 5u + 1u + 1u, 0u); Require(serializer.Serialize(request) == EncodeBase64(upgraded)); auto truncated = bytes; truncated.pop_back(); @@ -246,11 +254,12 @@ int main() { layerRejected = std::string_view(error.what()) == "RequestSerializer: truncated data"; } Require(layerRejected); - layered[4] = 36u; + layered[4] = 42u; + InsertMeshBarycentricFlag(layered); layered.insert(layered.begin() + 259u, {0u, 0u}); InsertPixelDefaultsAfterFrontFace(layered); - if (fixture.tessellation) layered.insert(layered.end() - 35u, 5u, 0u); - layered.insert(layered.end(), sizeof(std::uint64_t) + 1u + 5u + 1u, 0u); + if (fixture.tessellation) layered.insert(layered.end() - 35u, 6u, 0u); + layered.insert(layered.end(), sizeof(std::uint64_t) + 1u + 5u + 1u + 1u, 0u); Require(serializer.Serialize(layerReplay.request) == EncodeBase64(layered)); } } @@ -271,10 +280,11 @@ int main() { Require(replay.request.context.pixel->rasterPrimitive == ShaderRecompiler::RasterPrimitive::Rectangle); Require(replay.request.context.vertex->paClVsOutCntl == 0x00050a05u); Require(replay.request.target.float16DenormPreserve == preserve); - bytes[4] = 36u; + bytes[4] = 42u; + InsertMeshBarycentricFlag(bytes); bytes.insert(bytes.begin() + 259u, {0u, 0u}); InsertPixelDefaultsAfterFrontFace(bytes); - bytes.insert(bytes.end(), sizeof(std::uint64_t) + 1u + 5u + 1u, 0u); + bytes.insert(bytes.end(), sizeof(std::uint64_t) + 1u + 5u + 1u + 1u, 0u); Require(serializer.Serialize(replay.request) == EncodeBase64(bytes)); auto truncated23 = DecodeBase64(encoded23); @@ -394,12 +404,24 @@ int main() { Require(!fixture.tessellation || (request.graphics->tessellation->patchesPerGroup == 63u && request.graphics->tessellation->primitiveGroupSize == 37u && request.graphics->tessellation->breakAtInstance)); Require(request.graphics->draw.indexAddress == 0xabcdef3400u && request.graphics->draw.indexCount == 57u && request.graphics->draw.indexElementBytes == 4u && request.graphics->draw.instanceCount == 9u); Require(request.target.float16DenormPreserve == preserve); - auto upgraded = bytes; - upgraded[4] = 36u; - if (fixture.pixel) upgraded.insert(upgraded.begin() + 259u, {0u, 0u}); - if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(upgraded); - upgraded.insert(upgraded.end(), sizeof(std::uint64_t) + 1u + 5u + 1u, 0u); + auto main38 = bytes; + main38[4] = 38u; + InsertMeshBarycentricFlag(main38); + if (fixture.pixel) main38.insert(main38.begin() + 259u, {0u, 0u}); + if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(main38, false); + auto upgraded = main38; + upgraded[4] = 42u; + if (fixture.pixel) upgraded.insert(upgraded.begin() + 317u, 16u, 0u); + if (fixture.tessellation) upgraded.insert(upgraded.end() - 35u, 1u, 0u); + main38.insert(main38.end(), sizeof(std::uint64_t) + 1u + 5u + 1u, 0u); + upgraded.insert(upgraded.end(), sizeof(std::uint64_t) + 1u + 5u + 1u + 1u, 0u); Require(serializer.Serialize(request) == EncodeBase64(upgraded)); + const auto replay38 = serializer.Deserialize(EncodeBase64(main38)); + Require(replay38.request.graphics.has_value() && replay38.request.graphics->tessellation.has_value() == fixture.tessellation); + Require(!fixture.tessellation || (!replay38.request.graphics->tessellation->mergedHull && replay38.request.graphics->tessellation->primitiveGroupSize == 37u && replay38.request.graphics->tessellation->breakAtInstance)); + Require(replay38.request.graphics->draw.indexAddress == 0xabcdef3400u && replay38.request.graphics->draw.indexCount == 57u && replay38.request.graphics->draw.indexElementBytes == 4u && replay38.request.graphics->draw.instanceCount == 9u); + Require(replay38.request.target.float16DenormPreserve == preserve); + Require(serializer.Serialize(replay38.request) == EncodeBase64(upgraded)); for (const bool dither : {false, true}) { if (!fixture.pixel && dither) continue; auto main25 = bytes; @@ -408,12 +430,14 @@ int main() { const auto replay25 = serializer.Deserialize(EncodeBase64(main25)); Require(!fixture.pixel || (replay25.request.context.pixel->layerExported == layer && replay25.request.context.pixel->ditheredAlphaToCoverage == dither && replay25.request.context.pixel->reversedBlend == 0u)); Require(replay25.request.target.float16DenormPreserve == preserve && (!fixture.tessellation || replay25.request.graphics->tessellation->primitiveGroupSize == 37u)); - main25[4] = 36u; + main25[4] = 42u; + InsertMeshBarycentricFlag(main25); if (fixture.pixel) { main25.insert(main25.begin() + 260u, 0u); InsertPixelDefaultsAfterFrontFace(main25); } - main25.insert(main25.end(), sizeof(std::uint64_t) + 1u + 5u + 1u, 0u); + if (fixture.tessellation) main25.insert(main25.end() - 35u, 1u, 0u); + main25.insert(main25.end(), sizeof(std::uint64_t) + 1u + 5u + 1u + 1u, 0u); Require(serializer.Serialize(replay25.request) == EncodeBase64(main25)); } } @@ -424,13 +448,19 @@ int main() { for (const bool preserve : {false, true}) { request.target.float16DenormPreserve = preserve; request.target.float32FlushToZero = !preserve; + request.target.float32DenormPreserve = preserve; const auto encoded = serializer.Serialize(request); const auto replay = serializer.Deserialize(encoded); - Require(replay.request.target.float16DenormPreserve == preserve && replay.request.target.float32FlushToZero == !preserve); + Require(replay.request.target.float16DenormPreserve == preserve && replay.request.target.float32FlushToZero == !preserve && replay.request.target.float32DenormPreserve == preserve); auto bytes = DecodeBase64(encoded); - Require(bytes.size() >= 8u && bytes[4] == 36u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u); + Require(bytes.size() >= 8u && bytes[4] == 42u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u); + auto version40 = bytes; + version40.pop_back(); + version40[4] = 40u; + const auto replay40 = serializer.Deserialize(EncodeBase64(version40)); + Require(!replay40.request.target.float32DenormPreserve && replay40.request.target.float32FlushToZero == !preserve); auto future = bytes; - future[4] = 37u; + future[4] = 43u; bool futureRejected = false; try { static_cast(serializer.Deserialize(EncodeBase64(future))); @@ -468,11 +498,29 @@ int main() { locationRejected = std::string_view(error.what()) == "RequestSerializer: invalid PixelPositionLocation value"; } Require(locationRejected); + pixel.sampleOrder = {0x76543210u, 0x01234567u, 0x10325476u, 0x67452301u}; + request.context.pixel = pixel; + const auto orderEncoded = serializer.Serialize(request); + Require(serializer.Deserialize(orderEncoded).request.context.pixel->sampleOrder == pixel.sampleOrder); + auto version41 = DecodeBase64(orderEncoded); + const auto order = std::search(version41.begin(), version41.end(), pixel.sampleLocations.begin(), pixel.sampleLocations.end()) + pixel.sampleLocations.size() + 1; + Require(version41.end() - order > static_cast(sizeof(pixel.sampleOrder)) && *order == 0x10u); + version41.erase(order, order + sizeof(pixel.sampleOrder)); + version41[4] = 41u; + const auto replay41 = serializer.Deserialize(EncodeBase64(version41)); + Require(replay41.request.context.pixel.has_value() && replay41.request.context.pixel->sampleLocations == pixel.sampleLocations && replay41.request.context.pixel->sampleOrder == std::array{}); + pixel.sampleOrder = {}; + request.context.pixel = pixel; for (const bool spanned : {false, true}) { request.target.spannedWaves = spanned; Require(serializer.Deserialize(serializer.Serialize(request)).request.target.spannedWaves == spanned); request.target.float32FlushToZero = true; auto version35 = DecodeBase64(serializer.Serialize(request)); + version35.pop_back(); + const auto stencilOperationValue = std::search(version35.begin(), version35.end(), pixel.sampleLocations.begin(), pixel.sampleLocations.begin() + 4) + pixel.sampleLocations.size(); + const auto pixelTail = static_cast(1u + sizeof(pixel.sampleOrder)); + Require(version35.end() - stencilOperationValue > pixelTail && std::all_of(stencilOperationValue, stencilOperationValue + pixelTail, [](std::uint8_t value) { return value == 0u; })); + version35.erase(stencilOperationValue, stencilOperationValue + pixelTail); version35[4] = 35u; version35.pop_back(); const auto replay35 = serializer.Deserialize(EncodeBase64(version35)); diff --git a/demo/web/app.mjs b/demo/web/app.mjs index 8e05b091e0..4e0b534145 100644 --- a/demo/web/app.mjs +++ b/demo/web/app.mjs @@ -324,7 +324,7 @@ $("theme").onclick = () => { }; update(); lock(false); -if (supported) fetch("./release.json", { cache: "no-cache" }).then(response => { if (!response.ok) throw new Error(`Cannot load release information (${response.status})`); return response.json(); }).then(value => { +if (supported) fetch("./release.json", { cache: "no-cache" }).then(response => { if (!response.ok) throw new Error(`Cannot load release information (${response.status}).${response.status === 404 ? " release.json is generated during the site build, so it is missing when serving demo/web directly." : ""}`); return response.json(); }).then(value => { if (!value.tag || !["windows", "linux"].every(target => Number.isSafeInteger(value.platforms?.[target]?.expandedBytes))) throw new Error("Invalid release information"); release = value; update(); diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index a54c68ac3b..574244a6da 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -116,7 +116,7 @@ flowchart LR - A compute shader whose LDS plus its lock dword exceeds the host's `maxComputeSharedMemorySize` is not given a `Workgroup` array. `Recompile` sets `ShaderInfo::sharedMemoryBytes` for it and the SPIR-V backend addresses the LDS in a coherent storage buffer at descriptor set 1 (`WorkgroupMemoryDescriptorSet`), with a slice of `RecompileResult::workgroupMemoryDwords` per workgroup, indexed by `WorkgroupID` and `NumWorkGroups`. `VulkanDevice` keeps one such buffer, sized from the dispatch dimensions, and binds it after the resource set; programs that fit keep using shared memory. - `Driver::RegisterShader` splits shader preparation in two. `PlanRegistered` runs on the registering guest thread: it validates the registers and entry point, decodes the code and builds the recompile request, so invalid registrations still fail there. The RDNA to IR translation and resource plan (`PrepareShader`) run on a background pool of `hardware_concurrency() / 2` detached threads (at least 2, 64 MiB stacks). - A thread that needs the GPU mutex, the committer's next item or the end of the committer's queue spins before it blocks (`HandoffSpin.hpp`): `GpuMutexType::lock` retries `try_lock` for the window, the committer polls its item count, and `DrawPipeline::Drain` polls it for twice the window, so a hand-off that completes within the window costs no kernel wake-up. The window is 150 us when the process may run on more than 8 logical processors (its affinity mask, via `CpuTopology`) and 0 otherwise, where the spinning threads would compete with the holder; `APS5_HANDOFF_SPIN_US=` sets it (0 turns it off, at most 10000). Ordering and the lock's semantics are unchanged. -- Queue 0's packet worker decodes, captures and prepares a draw and hands its recording (`Graphics::Draw` under the GPU lock) to a committer thread in [`DrawPipeline.cpp`](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp), so the next packet is decoded while the previous draw is recorded. Each queued item carries the guest ranges it will write (`DrawWriteRanges`: written buffers and storage images, color targets with their DCC and CMASK, depth and stencil). A later packet that reads or writes one of those ranges, or that the pipeline can't order, drains it first; labels, submission ends, buffer fills and (with `APS5_PIPELINE_DMA=1`) DMA packets are queued as items instead. With `APS5_PIPELINE_DISPATCH=1` (off by default), a `DISPATCH_DIRECT` whose dispatch-cache entry hits with a recipe is queued as an item too, carrying the dispatch's written buffers and storage images (`DispatchWriteRanges`); the capture and the cache lookup stay on the worker and the recipe is recorded on the committer. A dispatch still drains when its lookup misses, when what it reads overlaps a write still pending, when the hit is verified against data or when its writes cannot be ranged (bindless access, GDS, fault buffer). An exception thrown by a commit is rethrown on the worker at its next enqueue or drain, as an inline draw error would be. On by default; `APS5_PIPELINED_DRAWS=0` records every draw inline, and `APS5_EXACT_DRAW_WRITES=0` widens the color and depth ranges to estimates. +- Queue 0's packet worker decodes, captures and prepares a draw and hands its recording (`Graphics::Draw` under the GPU lock) to a committer thread in [`DrawPipeline.cpp`](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp), so the next packet is decoded while the previous draw is recorded. Each queued item carries the guest ranges it will write (`DrawWriteRanges`: written buffers and storage images, color targets with their DCC and CMASK, depth and stencil). A later packet that reads or writes one of those ranges, or that the pipeline can't order, drains it first; labels, submission ends, buffer fills and (with `APS5_PIPELINE_DMA=1`) DMA packets are queued as items instead. With `APS5_PIPELINE_DISPATCH=1` (off by default), a `DISPATCH_DIRECT` whose dispatch-cache entry hits with a recipe is queued as an item too, carrying the dispatch's written buffers and storage images (`DispatchWriteRanges`); the capture and the cache lookup stay on the worker and the recipe is recorded on the committer. A dispatch still drains when its lookup misses, when what it reads overlaps a write still pending, when the hit is verified against data or when its writes cannot be ranged (bindless access, GDS, fault buffer). An indirect draw resolved on the CPU (its records read by the worker) drains only when its records or its count overlap a queued write; the records it draws inline drain first, and its last drawn record is queued like a direct draw (`APS5_NO_PIPELINED_INDIRECT=1` drains at every indirect draw). An exception thrown by a commit is rethrown on the worker at its next enqueue or drain, as an inline draw error would be. On by default; `APS5_PIPELINED_DRAWS=0` records every draw inline, and `APS5_EXACT_DRAW_WRITES=0` widens the color and depth ranges to estimates. - A dispatch's resource build runs stage A (binding plan, buffers, descriptor set) without the GPU lock and stage B (image lookups) under it. Between them `VulkanDevice::presync` waits, still without the lock, for recorded work that writes a surface stage B would read on the CPU and for recorded work that writes the DCC keys of an image the dispatch binds (`ShaderResources::PresyncSurfaces`), so the locked `WaitForKeyWriters` finds nothing pending to synchronize with. A key range is waited for only when recorded work writes it, the key-store memo does not give its value (`DccKeyWritersPending`, without the mutex) and the pending stores do not give it either (`DccKeyWriterNeedsSync`, under the mutex, the same test `WaitForKeyWriters` makes before it synchronizes). Stage A's own key scan runs before that wait, and stage B compares it with the keys it reads under the lock as before. `APS5_NO_DCC_PRESYNC=1` leaves the key ranges out; `APS5_NO_PRESYNC=1` disables the whole pre-sync. - The shader registry maps a code address to every header registered for that code, in registration order. Static, graphics and rectangle ABI resolution pick the header at the bound address (or one equal to it, `SameHeader`, which also takes a hull header fused by `sceAgcFuseShaderHalves` (type 3) as the registered back half (type 7) it copies, the fused registers aside; graphics ABI resolution then uses the fused header's shader registers); draws and dispatches pick the program by the stage's binary type (`RegisteredProgram`), and a front entry matching both type 2 and type 4 throws. Within a type the last registration wins; re-registering an identical earlier header makes its snapshot current again without preparing it again, and changed code drops the older headers. - The snapshot is published as pending. `SourceHandleFor`, `InvocationFor` and `ShaderPreparationTransaction::Edit` / `Read` wait for the pending job before reading `PreparedShaders`, and rethrow its failure. `APS5_SYNC_SHADER_PREPARE=1` prepares on the registering thread instead. @@ -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..4cebf79a20 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: @@ -174,7 +192,9 @@ On a device that supports `VK_EXT_device_fault`, the first `VK_ERROR_DEVICE_LOST Set `APS5_SAMPLE_WORKER=` to sample queue worker 0 every millisecond (in practice every timer tick). Every 20 s and at exit it writes ``, one `module offset self inclusive` line per sampled address, and `.collapsed.txt`, the worker's whole stacks in the collapsed format that speedscope.app and flamegraph.pl read: one `frame;...;frame count` line per distinct stack, root first, each frame as `module+0xRVA`. A walk that ends before the thread's root starts with `[truncated]`. The `#` lines give the samples, the walk depth (64 frames), the truncated walks, the mean and longest time the worker was suspended, and the size and PE timestamp of each module, to check the binary a file is symbolized against. A frame with `.pdata` is stepped by `RtlVirtualUnwind`; any other frame by its DWARF record in `.ehfram`, the unwind data the libraries are built with. Those steps read only inside the worker's stack, and a module is indexed after the worker resumes, the first time a walk meets it. -`APS5_SAMPLE_THREADS=` samples every thread of the process every 2 ms and writes the 40 hottest addresses of each busy thread to ``, 12 frames deep and through `.pdata` only. Both samplers exist only on Windows; the addresses are symbolized offline, for example with `nm` on the `.prx`. +`APS5_SAMPLE_COMMITTER=` samples queue 0's draw committer the same way and writes the same two files. The committer records the draws the worker hands over (`APS5_PIPELINED_DRAWS`), so when the worker's stacks wait in `DrawPipeline::Enqueue`, its stacks show what bounds the frame. It starts with the first pipelined draw; with `APS5_PIPELINED_DRAWS=0` nothing is sampled. + +`APS5_SAMPLE_THREADS=` samples every thread of the process every 2 ms and writes the 40 hottest addresses of each busy thread to ``, 12 frames deep and through `.pdata` only. The samplers exist only on Windows; the addresses are symbolized offline, for example with `nm` on the `.prx`. ## Performance trace @@ -184,7 +204,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 +220,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/CONVENTIONS.md b/docs/dev/CONVENTIONS.md index deb2f6a380..87c674dac4 100644 --- a/docs/dev/CONVENTIONS.md +++ b/docs/dev/CONVENTIONS.md @@ -14,4 +14,15 @@ Comments in code can only be added to indicate areas of [technical debt](Technic [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) -There's no need to add image files to the repository. You can add images to the [gist](https://gist.github.com/boykopovar/0e53f2e1426f29ecd41e3b51540b8a90) comments and paste a link into the md file. \ No newline at end of file +There's no need to add image files to the repository. You can add images to the [gist](https://gist.github.com/boykopovar/0e53f2e1426f29ecd41e3b51540b8a90) comments and paste a link into the md file. + +### Host writes into guest memory + +Host code that writes into guest memory through the kernel or a native library must hold a `GuestArena::HostWrite` ([GuestArena.hpp](../../core/libs/prx/libc/include/GuestArena.hpp)) over the destination range for the whole write. This covers file reads (`ReadFile`, `read`, `pread`), socket receives, `DeviceIoControl` and other calls that fill a caller's buffer. + +The write watch can leave guest pages read-only between collections: every watched page on macOS, and stable resident runs of at least 256 KiB on Windows (see the write-watch entries in [debt/libc.md](debt/libc.md)). A kernel-mode write into such a page doesn't fault into the watch. It fails instead, with `EFAULT` on macOS and `ERROR_NOACCESS` on Windows. `HostWrite` opens the pages and makes sure the write is recorded. + +- A write that completes later on another thread (overlapped I/O, completion ports, asynchronous reads) is bracketed where the bytes land, not where the request was issued. +- Ordinary CPU stores from host C++ code need no bracket: the write watch records them (through a fault where the page is protected). +- A GPU import of guest memory uses `HostImportAccess`, not `HostWrite`. +- Third-party code that writes guest memory on its own (driver threads, library worker threads) can't be bracketed. List it in that library's debt file. diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index ace7ce8e5c..7d6f4d537b 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 | @@ -140,6 +142,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_FILL_SYNC` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | waits for every recorded store over the fill's range | | `APS5_FLIP_INFLIGHT` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | 1 | number of presentations that may trail on the GPU (0, 1 or 2) | | `APS5_FLIP_READ_CHECK` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | records the in-place reads of completed batches and checks them before the present blit's fence | +| `APS5_FORCE_SDR` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/HostHdr.cpp` | unset | suppresses advertised HDR availability while retaining detected host capabilities; uses the existing SDR presentation path | | `APS5_FORCE_STRUCTURIZER_FALLBACK` | presence | `core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp` | | makes the control-flow test expect the reach-flag fallback instead of no cloning | | `APS5_FULL_BARRIERS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | records every barrier instead of merging redundant ones | | `APS5_GPU_CHECKPOINTS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | attaches device diagnostic checkpoints through VK_NV_device_diagnostic_checkpoints | @@ -152,8 +155,9 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_HOOK_FLUSH_CPU_BLOCKS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | set to 0 flushes pending images whose every 64 KiB block the CPU wrote, instead of skipping them | | `APS5_HOOK_FULL_SYNC` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | makes the flush hook's recorded-store wait a full Sync under the GpuMutex | | `APS5_HOOK_LOCKED_WAIT` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | keeps the flush hook's pending-write wait under the GpuMutex | -| `APS5_HOST_IMPORT_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | 6144 | budget in MiB for pinned host imports; 6144 is the floor, and an unset or 0 value uses the largest host heap minus 2 GiB | +| `APS5_HOST_IMPORT_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | 0 | budget in MiB for pinned host imports; `0` selects the larger of 6144 MiB and half the physical memory, at most the largest host heap minus the larger of 2 GiB and half that heap (4055 MiB on a 16 GB Windows PC with an RTX 4060) | | `APS5_IGNORE_DEPTH_TEST` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp` | | renders depth- and stencil-tested draws without a depth target as if the tests always passed | +| `APS5_IMAGE_POOL_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp` | | size in MiB of the released image memory the buffer pool keeps for reuse by memory type and size while the device-local heap is under its reported budget, a quarter of the device tier's budget by default; `0` frees image memory at once | | `APS5_INDIRECT_VERTEX_MIB` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/IndirectDraw.cpp` | 64 | cap in MiB on a vertex attribute extent, above which a GPU-side indirect draw resolves on the CPU | | `APS5_INSERT_COMPARE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp` | | compares a fresh capture's regions against the recorder before inserting it into the cache | | `APS5_KEPT_DESCRIPTOR_SETS` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | 8 | number of freed dedicated image table descriptor sets kept for a later build with the same layout and sizes; 0 keeps none | @@ -190,6 +194,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_BLOCK_TRACKING` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | tracks array layers, or the whole surface, instead of 64 KiB blocks | | `APS5_NO_BUFFER_CLASSES` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp` | | matches exact sizes in the buffer pool instead of rounding up to size classes | | `APS5_NO_CACHED_PRECOLLECT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | leaves the write-watch walks of a cached object's Revalidate under the GPU mutex | +| `APS5_NO_CLEAR_HAND_OVER` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | writes other images' pending results back at a clear-key storage refresh instead of handing them over to the refreshed image | | `APS5_NO_CLEARED_VIEW` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | keeps fast-cleared surfaces as CPU snapshots instead of viewing the cleared storage image | | `APS5_NO_COLLECT_MEMO` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | disables the per-thread memo of write-watch collects | | `APS5_NO_CONTEXT_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | rebuilds the Graphics::Context on every call instead of returning the cached one | @@ -225,21 +230,23 @@ 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 | | `APS5_NO_GPU_SAMPLE_DUMPS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | dumps occlusion-counter samples on the CPU instead of through the GPU copy path | +| `APS5_NO_GPU_THREAD_DIMENSIONS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/IndirectDispatch.cpp` | | reads the thread counts of an indirect dispatch with USE_THREAD_DIMENSIONS on the CPU instead of turning them into group counts and a thread limit on the GPU | | `APS5_NO_HOLD_PROFILE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | turns off GpuMutex hold-time profiling, keeping only the wait timing | | `APS5_NO_HOOKSYNC_PROFILE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | leaves only the plain hook-sync counts under APS5_PROFILE_DRAW | | `APS5_NO_HOOK_COMPLETION_GUARD` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | lets the flush hook record the accessing worker's queued labels even from inside a completion action | | `APS5_NO_HOOK_SNAPSHOT` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | takes the GpuMutex on every access instead of using the pending-write snapshot | | `APS5_NO_HOST_IMPORT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | disables importing guest memory, so address-based shaders copy instead of using it in place | | `APS5_NO_HW_F32_FLUSH` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | skips the startup f32 denormal flush probe and keeps the emulated flush in every shader | +| `APS5_NO_HW_F32_PRESERVE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | stops requesting DenormPreserve for 32-bit floats, so the device keeps its default f32 denormal handling | | `APS5_NO_IMAGE_PAGE_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | leaves the main guest image's page-protection table uncached | | `APS5_NO_INDIRECT_DRAW` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp` | | rejects indirect draw packets at submission validation | | `APS5_NO_JOIN_WAW_CHECK` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | lets a contiguous store join the pending one without the write-after-write check | -| `APS5_NO_KEEP_NEW_STORAGE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | does not keep a newly created storage image alive with the open batch | +| `APS5_NO_KEEP_NEW_STORAGE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | does not keep a newly created storage image alive with the open batch; such an image evicted before the batch completes can hand its memory to another image through the buffer pool while the batch still writes it | | `APS5_NO_KEYS_FILL_MEMO` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp` | | records no memo of GPU-side DCC key fills | | `APS5_NO_KEYS_REFILL` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | keeps a covered image's uploaded keys as they were instead of marking them uncompressed after a matching key fill | | `APS5_NO_KEYS_RESCAN` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | reuses the DCC keys scanned before the lookup's refresh instead of discarding them for a rescan | @@ -265,6 +272,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_PENDING_HTILE_FILLS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp` | | reads the HTILE words of a depth target bind even when recorded fills are pending over them, instead of working the words out from the fills | | `APS5_NO_PERF_FRONTEND_PAIR` | presence | `core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp` | | disables frontend-pair reuse in the shader-memory test | | `APS5_NO_PERF_LAYOUT_PAIR` | presence | `core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp` | | disables layout-pair reuse in the shader-memory test | +| `APS5_NO_PIPELINED_INDIRECT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp` | | drains the draw pipeline at every CPU-path indirect draw instead of queuing it behind the pipeline | | `APS5_NO_PIPELINE_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp` | | builds a pipeline per draw instead of reusing cached ones | | `APS5_NO_PIPELINE_JOURNAL` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp` | | keeps the pipeline journal off; 0 leaves it on, and it is also off while the shader disk cache is disabled | | `APS5_NO_PIPELINE_LTO` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp` | | disables background link-time optimization of graphics pipeline libraries | @@ -274,6 +282,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_PRESENT_EPOCH` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | keeps the presenter at collect epoch 0 instead of adopting the newest epoch a worker started | | `APS5_NO_PRESYNC` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | disables the stage-A pre-sync of pending writes | | `APS5_NO_PROC_TABLE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | resolves the device's Vulkan entry points per call instead of filling the function table once | +| `APS5_NO_QUEUED_DRAW_READ_ORDER` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp` | | stops queued draws and DMA copies from holding back write-backs over the guest memory they read at commit | | `APS5_NO_QUEUED_LABELS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/DeferredLabels.cpp` | | disables the queued-label table | | `APS5_NO_RECORDED_CLEAR` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | waits for a storage image's fast clear in a batch of its own instead of recording it | | `APS5_NO_RECORDER_SYNC_DRAWS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | gives the draws the recorder waits for their own batch instead of recording them with the wait | @@ -290,6 +299,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_SITE_SAMPLING` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp` | | disables per-site sampling of write evidence | | `APS5_NO_SNAPSHOT_CHECK` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | skips the live compare that proves a captured snapshot still equals the guest bytes | | `APS5_NO_SORTED_SNAPSHOT_LOOKUP` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | scans the snapshot's regions in AddSnapshot instead of searching them | +| `APS5_NO_SPACE_STAMP_ONCE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | stamps every direct import of a cached address space on each hit instead of once per space | | `APS5_NO_SPARSE_HEAP_MIRROR` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | disables sparse heap mirrors, mirroring only whole heap ranges | | `APS5_NO_STAGE_A_IMAGES` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | leaves every image element to the stage-B full lookup instead of a stage-A record | | `APS5_NO_STORAGE_DEDUPE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | looks each identical consecutive storage descriptor up, and refreshes its image, separately | @@ -315,6 +325,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_WORKER_AFFINITY` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/WorkerAffinity.cpp` | | leaves the queue workers and presenter unpinned (affinity mask 0) | | `APS5_NO_WRITER_KEYED_EVIDENCE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp` | | records written-element evidence without keying it by writer | | `APS5_NO_WRITE_EVIDENCE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp` | | disables the written-element evidence that lets a read skip the flush hook | +| `APS5_NO_WRITE_ONLY_PASSES` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | counts a buffer a draw only stores to as read and written, so the next draw storing to it begins a new render pass | | `APS5_NO_WRITTEN_SHADOW` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | binds written staging elements in place instead of through the written shadow | | `APS5_PACKET_EPOCH` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitStatistics.cpp` | | makes queue workers bump the collect epoch before every packet | | `APS5_PIPELINED_DRAWS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp` | 8 | depth of the queue-0 draw pipeline: 0 records draws inline, 1 selects the default depth of 8, and any other value is the depth capped at 256 | @@ -342,11 +353,14 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_RELEASE_UNDER_LOCK` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | destroys deferred batches in finish() under the lock instead of on a release thread | | `APS5_RESOURCE_CACHE_ENTRIES` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | 1024 | maximum entries in the per-device shader-resource cache | | `APS5_RESOURCE_CACHE_TABLE_ELEMENTS` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | 65536 | number of image-table elements the shader-resource cache keeps (0 keeps the default) | +| `APS5_SAMPLE_COMMITTER` | string | `core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp` | | samples queue 0's draw committer every millisecond and writes its stacks to a file (Windows only) | | `APS5_SAMPLE_THREADS` | string | `core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp` | | samples every thread of the process every 2 ms and writes each busy thread's hottest frames to a file (Windows only) | | `APS5_SAMPLE_WORKER` | string | `core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp` | | samples the calling queue worker every millisecond and writes its stacks to a file (Windows only) | | `APS5_SHADOW_VERIFY` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp` | | verifies that the shadow-published bytes equal the guest bytes | | `APS5_SHARED_COLLECT_MEMO` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | uses the tracker's shared collect-memo ring under its mutex instead of the per-thread ring | | `APS5_SHARED_MEMORY_COUNTERS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | accumulates the memory counters into one shared set with atomic RMWs | +| `APS5_SKIP_INVALID_INDIRECT_REGISTERS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp` | | skips the entries whose offset dword is no register offset in a SET_*_REG_INDIRECT list read again where its packet executes, and writes its valid entries, instead of stopping | +| `APS5_SKIP_REFUSED_DRAWS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp` | | skips a draw whose graphics stage the recompiler refuses, reporting each refused shader once, instead of failing the queue | | `APS5_SKIP_REQUEST_DUMPS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderDiagnostics.cpp` | 8 | caps RecompileRequest dumps per shader-skip reason | | `APS5_SKIP_SHADER` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp` | 0 | hex program address of a shader to skip preparing | | `APS5_SNAPSHOT_CHECK_FATAL` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | throws on the first snapshot that differs from registered memory instead of only counting them | @@ -370,12 +384,14 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_SYNC_SAMPLE_DUMPS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | takes the CPU path for occlusion-counter sample dumps, as `APS5_NO_GPU_SAMPLE_DUMPS` does | | `APS5_SYNC_SHADER_PREPARE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp` | | prepares registered shaders on the registering thread instead of in the background | | `APS5_SYNC_TEXTURE_UPLOAD` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | restores the waiting batch for every texture upload | +| `APS5_TEMPLATE_AUDIT_MS` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | 1000 | milliseconds after which a cached shader-resource object takes the full walk instead of the fast proof; `0` turns the audit off | | `APS5_TEXTURE_CACHE_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | 0 | forced byte budget in MiB for the storage and sampled texture caches; `0` selects the budget automatically | | `APS5_THREAD_NICE` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp` | -10 | nice value the raised queue workers and presenter ask rtkit for (clamped to -20..19) | | `APS5_THREAD_PRIORITY` | string | `core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp` | off | raises the queue workers and presenter above the title's threads: `off`, `high` or `rt` | | `APS5_THREAD_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 | @@ -403,7 +419,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_TRACE_SHADOW` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp` | | prints the first n shadow publishes, the first 64 seeds, every evict and retire | | `APS5_TRACE_SKIPS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderDiagnostics.cpp` | | reports each distinct shader skip once | | `APS5_TRACE_STAGING` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | prints every distinct staged range once, with its size and whether an atomic element lies in it | -| `APS5_TRACE_SUMMARY_EVERY` | integer | `core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp` | | number of presents between the trace summary lines | +| `APS5_TRACE_SUMMARY_EVERY` | integer | `core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp` | | seconds between periodic trace summaries, emitted on the next present | | `APS5_TRACE_TEXTURES` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | prints a sampled texture's bytes and how many are nonzero | | `APS5_TRACE_UNREADABLE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | names the code that checked an inaccessible range | | `APS5_TRACE_UPLOAD` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | names why a storage image is uploaded again | @@ -503,6 +519,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `ANYPS5_INPUT_CONFIG` | string | `core/libs/prx/libSceVideoOut/src/InputMapping.cpp` | | path of the input mapping file that binds host keys and mouse to guest pad actions | | `ANYPS5_REQUIRE_DISPLAY` | presence | `core/libs/prx/libSceVideoOut/tests/Flip.cpp` | | makes video-out tests fail instead of skipping when no display or Vulkan device is available | | `APS5_NO_PAD_INPUT` | presence | `core/libs/prx/libSceVideoOut/src/PadInput.cpp` | | ignores keyboard and mouse input so a measurement run is not advanced by a stray press | +| `APS5_VIDEOOUT_TELEMETRY_DIR` | string | `core/libs/prx/libSceVideoOut/src/Telemetry.cpp` | | directory where videoout.jsonl is appended with the pad and present telemetry lines; nothing is opened when unset or empty | ### libSceVideodec2 @@ -524,7 +541,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_WRITE_WATCH` | presence | `core/libs/prx/libc/src/GuestArena.cpp` | | disables write watching so guest buffers are fully re-read on every use | | `APS5_PIN_WAIT_MS` | integer | `core/libs/prx/libc/src/GuestAllocations.cpp` | 60000 | milliseconds to wait for a pinned allocation lease before failing | | `APS5_TRACE_AFFINITY` | presence | `core/libs/prx/libc/include/CpuTopology.hpp` | | prints a line naming the role, tid and mask of every thread pinning | -| `APS5_TRACE_CALLS` | string | `core/libs/prx/libc/src/specifics/windows/CallTrace.cpp` | | comma-separated libraries (or all) whose guest calls are traced | +| `APS5_TRACE_CALLS` | string | `core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp` | | comma-separated libraries (or all) whose guest calls are traced | | `APS5_TRACE_EXIT` | presence | `core/libs/prx/libc/src/Process.cpp` | | prints a backtrace of the caller of exit, _Exit and the process-exit path | | `APS5_TRACE_SSE4A` | presence | `core/libs/prx/libc/src/specifics/windows/CrashReport.cpp` | | prints each emulated SSE4a instruction once per rip | | `APS5_WATCH_ADDR` | integer | `core/libs/prx/libc/src/specifics/windows/CrashReport.cpp` | | absolute hex address whose writes are reported after its page is committed | @@ -538,7 +555,9 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_COARSE_TIMED_WAITS` | presence | `core/libs/prx/libkernel/Time/TimedWait.cpp` | | on Windows, rounds timed waits up to the scheduler tick | | `APS5_JOB_AFFINITY` | presence | `core/libs/prx/libkernel/Pthread/src/Thread.cpp` | | pins the title's job worker threads to the efficiency cores of a hybrid CPU | | `APS5_JOB_AFFINITY_MASK` | integer | `core/libs/prx/libkernel/Pthread/src/Thread.cpp` | | hex core mask that job worker threads are pinned to | +| `APS5_NO_DIRECT_MEMORY_SECTION_POOL` | presence | `core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp` | | on Windows, creates a section for every direct-memory allocation and closes it at release instead of reusing released sections that never held guest data | | `APS5_NO_JOB_AFFINITY` | presence | `core/libs/prx/libkernel/Pthread/src/Thread.cpp` | | disables pinning of job worker threads | +| `APS5_NO_SLEEP_WAKE` | presence | `core/libs/prx/libkernel/Time/SleepWake.cpp` | | keeps every sleep of 1 ms or less in full instead of ending it at a GPU batch, batch completion action or flip completion | | `APS5_NO_TIMER_RESOLUTION` | presence | `core/libs/prx/libkernel/Time/Time.cpp` | | keeps the default 15.6 ms scheduler tick instead of raising it to 0.5 ms on Windows | | `APS5_TIME_SCALE` | integer | `core/libs/prx/libkernel/Time/Time.cpp` | 1 | factor applied to the guest monotonic clocks and TSC; sleeps still take real time | | `APS5_TRACE_APR` | presence | `core/libs/prx/libkernel/Apr/src/Apr.cpp` | | logs every APR file read with its path, offset, size and destination | diff --git a/docs/dev/GAME_TESTING.md b/docs/dev/GAME_TESTING.md index be0924205b..73d6271fc2 100644 --- a/docs/dev/GAME_TESTING.md +++ b/docs/dev/GAME_TESTING.md @@ -10,6 +10,7 @@ For user options see [USAGE](../user/USAGE.md); for internals see [ARCHITECTURE] - Convert the title from plain ELF files (`tools/unwrap_self.py` unwraps a dump's `eboot.bin` and `.prx`): - Linux: `relinker --registry src/eboot.elf app.elf` - Windows: `relinker --windows --registry src/eboot.elf app.exe`. Without `--windows` the output is an ELF file whatever its name, and Windows refuses to start it (exit code 216, "not compatible with the version of Windows"). +- A dump whose `app0` holds `ampr_assets.index` and `ampr_assets-*.pak` was repacked for [ampr_emu](https://github.com/drakmor/ampr_emu): the packed files were removed from `app0`, and only ampr_emu reads the packs. Restore them first with ampr_emu's `tools/ampr_pack.py unpack --index app0/ampr_assets.index --output ` and copy the files missing from `app0`. Without them a Unity title stops at start-up on the missing `Media/Resources/unity default resources`. - Convert every module the title loads from its own folders (`sce_module/`, `Media/Modules/`, `Media/Plugins/`). Unity titles need `--module-dir Media/Modules --module-dir Media/Plugins`. Put each printed `Guest module:` file next to its original `.prx` in `app0/`, and keep the original there. - `libs/` holds only AnyPS5's own libraries, from `build/core/libs/libs` (not `unpatched/`). A game module placed there fails to load (error 193 on Windows). A self-built `libs/` on Windows also needs `libgcc_s_seh-1.dll`, `libstdc++-6.dll` and `libwinpthread-1.dll` from the toolchain's `mingw64/bin`, or every library fails with error 126 ([USAGE](../user/USAGE.md#input-and-conversion)). - Run `tools/import_audit.py` on the registries. Exit code 1 lists imports that will fail or throw. 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/REVERSE_ENGINEERING.md b/docs/dev/REVERSE_ENGINEERING.md new file mode 100644 index 0000000000..bcbcd1ef3a --- /dev/null +++ b/docs/dev/REVERSE_ENGINEERING.md @@ -0,0 +1,93 @@ +# Reading titles and libraries + +Many exports have no source that gives their signature or behaviour. Two places still hold them: the code of a title +that calls the export (its call sites) and the code of the library itself. This page covers how to read both, what each +one settles, and how to cite it. + +## Sources, strongest first + +1. **A console measurement**: a homebrew payload that calls the function and records what it returns, with the + firmware version (for example `sceVoiceQoSEnd`, #1407). +2. **The library's own code**: a firmware module, or a copy of an SDK module that a title ships in `sce_module/`. It gives + behaviour, error codes, layouts and validation order ([libSceJobManager](debt/libSceJobManager.md), + [libSceRtc](debt/libSceRtc.md), [libSceHttp](debt/libSceHttp.md)). A module a title ships is not reimplemented + ([CONTRIBUTING](../../CONTRIBUTING.md)); it can still be read to learn how a system library behaves. +3. **Other implementations and SDK bindings**: KytyPS5, shadPS4, fpPS4, SharpProspero, Orbital. PS4 sources may differ + from the PS5 ABI. +4. **A title's call sites**: the arguments, their widths, buffer sizes, which pointers are outputs and which results the + title checks. They show what the caller expects, not what the library does. + +When sources disagree, say so in the pull request and in the debt entry. + +## What a call site settles + +The PS5 uses the System V AMD64 calling convention: integer and pointer arguments in `rdi`, `rsi`, `rdx`, `rcx`, `r8`, +`r9`, then the stack; floating-point arguments in `xmm0`-`xmm7`; the result in `rax` or `xmm0`. + +| Seen at the call site | Settles | +| --- | --- | +| Registers written right before the `call` | The argument count. A register set long before the call, or for another use, is not an argument; check several call sites | +| `mov %esi` / `mov %rsi`, `movzwl`, `movsbl` | The argument width and signedness | +| `lea -0x2030(%rbp), %rsi` | A pointer to a caller buffer; the stack layout gives its size | +| A value stored at the pointer before the call and read after it | An in/out argument (capacity in, size out) | +| `test %eax, %eax` / `cmp`, `js` after the call | An `int32_t` result and which values the title treats as failure | +| The result is never read | Nothing: the return type is not established ([libSceAgc](debt/libSceAgc.md)) | +| A log string that names the function (`"sceXxx() failed. Error code %#x"`) | Which import the call is, and that failures are SCE error codes | +| A first call with a null buffer, then a second one with a buffer of the returned size | A size query ([libSceNpTrophy2](debt/libSceNpTrophy2.md)) | + +A call site doesn't settle what the library writes, which error codes it returns, its side effects, or the values a +title happens to accept. "The title continues if it gets 0" is not "the console returns 0". Everything that the call +site and the other sources leave open throws (`NotImplemented_nid_no_patch`), as in +[libSceAudioIn](debt/libSceAudioIn.md): the signature came from a call site, the meaning of the result did not, so the +export still throws. A silent stub is allowed only when it affects the UI ([CONTRIBUTING](../../CONTRIBUTING.md)): a +read that always reports no data, or a write that drops its data, is not UI. + +## Finding the call sites + +1. Unwrap the title's decrypted `eboot.bin` with `tools/unwrap_self.py` + ([GAME_TESTING](GAME_TESTING.md)). +2. Relink it with `--registry` ([USAGE](../user/USAGE.md)). `app.registry.json` lists every import of the executable + with its `nid`, `relocationType`, `targetOffset` (the GOT slot) and `callSites` (virtual addresses). Bundled modules' + registries have no `callSites`. `tools/nid_names.py --name sceXxx` gives the NID of a name, and `--nid` the name of a + NID (without the `#` suffix). + - `R_X86_64_GLOB_DAT`: each call site is the `call *slot(%rip)` itself. + - `R_X86_64_JUMP_SLOT`: the call site is the PLT stub (`jmp *slot(%rip)`); the callers are the `call ` + instructions. +3. Disassemble around the site. The ELF has no section headers, so `objdump -d` prints nothing; take the executable + `LOAD` segment from `readelf -lW`, cut it out at its file offset and disassemble it at its virtual address: + + ```sh + readelf -lW eboot.elf # executable LOAD: offset 0x4000, vaddr 0x0, filesz 0x1f36afc + tail -c +$((0x4000 + 1)) eboot.elf | head -c $((0x1f36afc)) > text.bin + objdump -D -b binary -m i386:x86-64 --adjust-vma=0x0 text.bin > text.dis + grep -nE 'call +0x1f2e480$' text.dis # callers of the JUMP_SLOT stub at 0x1f2e480 + ``` + + A disassembler that loads the program headers (Ghidra, IDA) does the same. +4. Log strings near the call confirm which import it is: `strings -t x eboot.elf`, then the `lea` that loads that address. + +The same steps work on a module: the code of a firmware module or of one in `sce_module/` gives the library's side of +the contract. + +## Citing it + +In the pull request and in the library's [debt](debt/) entry, say: + +- Where it came from: the title ID and the call-site addresses (`PPSA03393, 0x76D132`), or the module and firmware + version (`libSceRtc.sprx, firmware 13.60`). +- What each site showed: "`rsi` points to a 0x2000-byte stack buffer and `rdx` to its capacity, which the title reads + back as the size". +- What was not verified: "read from disassembly, not measured on a console", "the return value is not read". + +Quote only the instructions that show the claim, not whole functions. The code stays generic: a title ID belongs in the +debt entry, never in code or test names. + +## Rules + +- Work only from dumps of titles and firmware that you own. Never add, attach or link game files, firmware, decrypted + binaries or keys in the repository, a pull request or an issue. +- Write the implementation from the contract you read (arguments, sizes, values, order, errors), not by translating the + disassembly instruction by instruction or copying its structure. Rebuild data tables from a public source when one + exists ([libSceIme](debt/libSceIme.md) rebuilds its codepage checks from Unicode's tables). +- The goal is interoperability: what a title needs from a system library to run. Nothing here is for bypassing + licence, entitlement or online checks. diff --git a/docs/dev/TRACING.md b/docs/dev/TRACING.md index 8297f4ab04..8cb84bb4a4 100644 --- a/docs/dev/TRACING.md +++ b/docs/dev/TRACING.md @@ -11,7 +11,7 @@ Set a switch before starting the emulator. Unless noted, any value enables it. M | `APS5_TRACE_APR` | APR file reads, including path, offset, size and destination. | | `APS5_TRACE_AUDIOOUT` | Audio output port parameters when a port is opened. | | `APS5_TRACE_AUDIOOUT2` | AudioOut2 context and port parameters, attributes, pushes, queue polls and periodic queue summaries. | -| `APS5_TRACE_CALLS` | On Windows, records calls to the imports selected by this comma-separated library list (or `all`); crash and abort reports include the last 64 calls and most-called imports. The executable must be converted with `--trace-imports`; see [Usage](../user/USAGE.md#tracing-system-library-calls). | +| `APS5_TRACE_CALLS` | On Windows and Linux, records calls to the imports selected by this comma-separated library list (or `all`); crash and abort reports (abort only on Linux) include the last 64 calls and most-called imports. The executable must be converted with `--trace-imports`; see [Usage](../user/USAGE.md#tracing-system-library-calls). | | `APS5_TRACE_EXIT` | Guest `exit` and `_Exit` calls and process-exit backtraces. | | `APS5_TRACE_FIBER` | Fiber initialization, switches and returns. | | `APS5_TRACE_MEMORY` | Direct-memory allocation, mapping and protection operations. | @@ -35,7 +35,7 @@ Set a switch before starting the emulator. Unless noted, any value enables it. M | `APS5_TRACE_COPY_HLE` | Pending host-side copies, their ranges, GPU batches and known writers. | | `APS5_TRACE_DCC_KEYS` | Display and storage-image DCC key reads, writes, fills and clears. | | `APS5_TRACE_DISPATCH_CACHE` | Compute dispatch cache hits, misses and changed user-data words. | -| `APS5_TRACE_DISPATCH_IO` | Compute dispatch resources; value `2` also prints small user-data buffers. | +| `APS5_TRACE_DISPATCH_IO` | Compute dispatch queue, submission serial, shader and resources; value `2` also prints small user-data buffers. | | `APS5_TRACE_DRAW_CACHE` | Draw-cache lookups, hits, misses, and key differences, reported every ten seconds. | | `APS5_TRACE_DRAWS` | Draw inputs and per-draw timing phases. With `APS5_PROFILE_DRAW`, also prints the detailed draw lines. | | `APS5_TRACE_DRAW_PROGRAMS` | One line per draw listing selected shader stages, roles, code addresses and entry addresses. | @@ -68,6 +68,7 @@ See [Build](BUILD.md#performance-trace) for the trace format, summary fields and | `APS5_TRACE_SUMMARY_EVERY=` | Writes periodic summary lines without recording trace events. Can be combined with either window switch. | | `APS5_TRACE_EVENTS=` | Sets the per-thread event-buffer capacity for a frame or seconds window (1–4,194,304; default 262,144). | | `APS5_TRACE_DIR=` | Writes performance capture and summary files to this directory instead of the working directory. | +| `APS5_VIDEOOUT_TELEMETRY_DIR=` | Writes the presents and the pad changes of a run to `/videoout.jsonl`; see [Usage](../user/USAGE.md#video-out-and-pad-telemetry). | ## Shader registers diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 86b594ae4e..ee2a2dfd75 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -24,6 +24,8 @@ 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. + +- [Quick syntax checks](../../tools/check_compile.py) reject unexpanded `@response` arguments rather than parse compiler-specific quoting and risk retaining output flags. Expand response files in `compile_commands.json` before checking; the Ninja fallback expands them with `ninja -t compdb -x`. diff --git a/docs/dev/debt/libSceAcm.md b/docs/dev/debt/libSceAcm.md index c5c9b90f30..59682c3221 100644 --- a/docs/dev/debt/libSceAcm.md +++ b/docs/dev/debt/libSceAcm.md @@ -6,5 +6,8 @@ ### 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). +- [`sceAcmBatchJobNotification`](../../../core/libs/prx/libSceAcm/Export.cpp) is a parameterless pending declaration for import resolution. Its signature and behavior are unverified; it throws through `NotImplemented_nid_no_patch`. diff --git a/docs/dev/debt/libSceAgc.md b/docs/dev/debt/libSceAgc.md index be6ef270d9..0bd4347123 100644 --- a/docs/dev/debt/libSceAgc.md +++ b/docs/dev/debt/libSceAgc.md @@ -2,7 +2,7 @@ ### Unknown function info -- [GS shader-half fusion](../../../core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp) (libSceAgc) - `SPI_SHADER_PGM_RSRC2_GS.OC_LDS_EN` is preserved if either half requests it, while user SGPR allocation comes from the front half and other state stays with the back half. Bit 18 is `OC_LDS_EN` in AMD's [GFX10 register definitions](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/include/asic_reg/gc/gc_10_1_0_sh_mask.h). This is inferred from retaining both halves' requirements; console fusion output has not been measured. Cocoon (PPSA08766) requests front `0x70004` and back `0xab0020`. Other unmatched hardware state still throws. +- [GS shader-half fusion](../../../core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp) (libSceAgc) - `SPI_SHADER_PGM_RSRC2_GS.OC_LDS_EN` is preserved if either half requests it, while user SGPR allocation comes from the front half and other state stays with the back half. Bit 18 is `OC_LDS_EN` in AMD's [GFX10 register definitions](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/include/asic_reg/gc/gc_10_1_0_sh_mask.h). Mesa's radeonsi sets it on a merged ES+GS program exactly when the ES stage is a tessellation evaluation shader ([`si_shader_gs_legacy`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/45ea3ed8ce649d6b72d1843307a29d9cc3b7704e/src/gallium/drivers/radeonsi/gfx/si_state_shaders.cpp#L631), [`gfx10_shader_ngg`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/45ea3ed8ce649d6b72d1843307a29d9cc3b7704e/src/gallium/drivers/radeonsi/gfx/si_state_shaders.cpp#L1144)), so it belongs to the front half; console fusion output has not been measured. Cocoon (PPSA08766) requests front `0x70004` and back `0xab0020`. Other unmatched hardware state still throws. - PPSA01341 imports declared without parameters, signatures unknown: [sceAgcSetSemaphoreMemory](../../../core/libs/prx/libSceAgc/Unimplemented.cpp). Name from the shadPS4 aerolib NID list - [sceAgcWaitRegMemPatchMask](../../../core/libs/prx/libSceAgc/Patch/src/WaitRegMem.cpp) (libSceAgc) - follows `sceAgcWaitRegMemPatchReference`: the mask is taken as a 32-bit value and written to the low mask word of a 32- or 64-bit wait, as [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) does; [sharpemu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/Agc/AgcExports.PacketPatching.cs) writes both words of a 64-bit wait's mask. A mask above 32 bits throws @@ -37,7 +37,10 @@ - [sceAgcUpdateInterpolantMapping_10000](../../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) follows [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c) only, not confirmed on hardware: the `sceAgcCreateInterpolantMapping_10000` transform without the identity tail, so it writes one register per pixel shader input and none for a pixel shader without inputs, and a null pixel shader throws. For an `is_f16` 1 or 3 input whose `default_value` and `default_value_hi` differ it throws: OpenAGC takes `DEFAULT_VAL` from `default_value_hi`, while KytyPS5, sharpemu and `sceAgcCreateInterpolantMapping_10000` here take it from `default_value` - [sceAgcAcbWaitUntilSafeForRendering](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - `(buffer, videoOutHandle, displayBufferIndex)`, the declared arguments, which PPSA12544 (01.000.010) passes inside `waitUntilSafeForRendering` and `waitUntilSafeForRendering VR` markers on an asynchronous compute command buffer, ignoring the result. It writes the same rendering-wait packet as `sceAgcDcbWaitUntilSafeForRendering`, which the driver now accepts on compute queues too; the wait blocks only that queue's worker. Whether the console encodes the compute variant differently is not known - [sceAgcGetDefaultCxStateFlat](../../../core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp) (libSceAgc) - copies up to `size` bytes (clamped to 2048 bytes, the 512-dword `AgcContextState`) from the default context state initialized from the version 8 public register default groups (CX, SH, UC), indexed by register offset, following OpenAGC `src/context_state.c`; returns `0x80890001` on null destination; version selection without `sceAgcInit` is unverified +- [sceAgcDcbSetIndexIndirectArgs, sceAgcDcbSetIndexIndirectArgsGetSize](../../../core/libs/prx/libSceAgc/DcbDraw/src/IndexBuffer.cpp) (libSceAgc) - the `(buffer, attributeAddress, attributeIndex)` signature, a 64-bit address and a 32-bit index, is [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L1742-L1757)'s. The console oracle in prosper measured the packet, an INDEX_ATTRIBUTES_INDIRECT (opcode 0x91) holding the low address dword, the high address dword and the index ([measured](https://github.com/mattias800/prosper/blob/3ea5cb9a846b6dcd00f5073360462bbda4f72288/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L172-L174)), and a size of 16 bytes ([measured](https://github.com/mattias800/prosper/blob/3ea5cb9a846b6dcd00f5073360462bbda4f72288/prosper/tests/data/console_oracle/agc.golden.tsv#L67)). The console clears the low four address bits and keeps the low 16 bits of the index; here a nonzero address that is not 16-byte aligned, or an index above 0xffff, throws, as `sceAgcDcbSetIndexBuffer` throws for a misaligned address. A null address is written as 0, as the console writes it. No title is known to call them. + +- [sceAgcSuspendPointAndCheckStatus](../../../core/libs/prx/libSceAgc/Export.cpp) - this named callable export is listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAgc.c) and was cross-checked against the same module in PS5 firmware 13.60. Its signature and behaviour are unverified; the declaration uses the no-parameter unknown-signature convention and throws through `NotImplemented_nid_no_patch`. ### Functional - [sceAgcDebugRaiseException](../../../core/libs/prx/libSceAgc/Misc/src/Debug.cpp) (libSceAgc) - logs the exception id and returns 0, as retail hardware does nothing per [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c); the trap (`ud2`) OpenAGC describes for development hardware is not reproduced. Neither is verified on a console. The `uint32` exception id is [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/6f0cddd4404ec5203e71fea77e75fc373299ba3f/src/libs/agc.cpp)'s claim; OpenAGC declares no arguments, so the logged id may be leftover register contents diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 102fd9cad7..747e85126a 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -3,11 +3,14 @@ ### Silent stubs - [sceAgcDriverRegisterDefaultOwner](../../../core/libs/prx/libSceAgcDriver/Unimplemented.cpp) (libSceAgcDriver) - takes one uint32 owner by value (OpenAGC `include/agcdriver.h`, SharpEmu `AgcExports.DriverResources.cs`) and returns 0 without recording it. PPSA21480 treats the code 0x8A6C9018 it returned before as fatal. Console behaviour is unverified: OpenAGC describes the function as a firmware 5.50 stub that returns 0x8A6C9018, SharpEmu returns 0 +- [sceAgcDriverSetResourceRegistrationInfo](../../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - returns 0 without storing the info, as the retail `libSceAgcDriver.sprx` body does; registration is unavailable, so nothing reads it back. ### Unknown function info - [sceAgcDriverSubmitCommandBuffer, sceAgcDriverSubmitMultiCommandBuffers](../../../core/libs/prx/libSceAgcDriver/Submit/src/CommandBuffer.cpp) (libSceAgcDriver) - `queue_context` is treated as a pointer to context whose queue identifier is stored 4 bytes in, matching KytyPS5 (`src/libs/agc.cpp` `AgcDriverSubmitCommandBuffer` / `get_driver_queue`). A null context returns `EINVAL` (22). Whether the console validates other context fields before queue dispatch is unverified +- [sceAgcDriverAcquireHDRScopesACQ, sceAgcDriverGetCoredumpAddress, sceAgcDriverHp3dLockEnd, sceAgcDriverHp3dLockStart, sceAgcDriverIsEsvmProcess, sceAgcDriverNopDataWritePacket, sceAgcDriverReleaseHDRScopesACQ, sceAgcDriverSetTargetRingForDiag, sceAgcDriverSetValueToSharedGds, sceAgcDriverSetWorkloadStreamInactive, sceAgcDriverSubmitReprojectionAcb, sceAgcDriverSuspendPointSubmitCdbg, sceAgcDriverSysGetGfxAppGpuResetStatus](../../../core/libs/prx/libSceAgcDriver/Unimplemented.cpp) (libSceAgcDriver) - signatures and behavior are unverified; these parameterless declarations only resolve imports and throw through `NotImplemented_nid_no_patch`. + - [Initial point context](../../../core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp) - new queues and CLEAR_STATE seed PA_SU_POINT_SIZE 0x00080008 and PA_SU_POINT_MINMAX 0xffff0000, matching every public context table in libSceAgc/Command/include/RegisterDefaultsData.hpp. Cocoon (PPSA08766) reaches geometry point output without writing these registers in the observed startup. Applying the SDK context defaults to initial/CLEAR_STATE driver context is inferred; GPU power-on reset state is not measured and is not the source of these values. Explicit writes and pushed/restored contexts retain their own values. @@ -15,11 +18,13 @@ - [Inactive vertex point size](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) - `PA_CL_VS_OUT_CNTL.USE_VTX_POINT_SIZE` is accepted for lines, triangles and rectangles with `GS_EN` and `HS_EN` clear, when polygon mode is disabled or both faces are filled. Points, patches and absent routing or rasterization registers still throw. Bit 16 selects the misc export's X component as PointSize ([AMD CIK register reference](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/programmer-references/CIK_3D_registers_v2.pdf), PA_CL_VS_OUT_CNTL); ignoring it for non-point rasterization follows [Vulkan PointSize](https://docs.vulkan.org/refpages/latest/refpages/source/PointSize.html). This is inferred, not measured on console hardware. XashPS5 (PPSA19111 v1.3) sets `0x01210000` on a filled triangle draw. - [RELEASE_MEM of the system counter](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (AGC driver) - a `RELEASE_MEM` with `DATA_SEL = 4` throws: the frequency and epoch of the console's 64-bit system counter are not known. [amdgpu's GFX10 packet header](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/amdgpu/nvd.h) names the value "send 64bit sys counter value", beside 3, the GPU counter, which the driver writes as a 100 MHz count of the host's steady clock. An older KFD header names 4 a CP performance counter instead. A PS4 title through #6977 writes it at the end of each frame. +- [Point and line polygon modes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - front and back polygon types that differ while both faces are drawn throw, as Vulkan has one `polygonMode` per draw. Points are drawn only one pixel wide (`PA_SU_POINT_SIZE = 0x00080008` inside `PA_SU_POINT_MINMAX`, the `VK_KHR_maintenance5` size of a point whose shader does not write `PointSize`); other sizes, a per-vertex size and devices without `maintenance5` throw. Lines must be one pixel wide (`PA_SU_LINE_CNTL`) and use Vulkan's default line rasterization. Depth bias with a point or line polygon mode, and such a mode on a rectangle list, throw. The fields follow the [AMD CIK register reference](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/programmer-references/CIK_3D_registers_v2.pdf); the console's rasterization of these points and lines is not measured. - [Native multisampled depth clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) accept the recognized HTILE mask kernel's direct full-range dispatch after its GPU writes complete. Every metadata byte needs tracked driver-store coverage since notification, no intervening CPU or depth write, and HTILE words proving the current clear value of zero or one. Partial, overlapping, indirect and unproven producers remain unsupported. Existing sampled views retain the native image; guest depth publication and metadata-fill transitions remain unimplemented. Windows shared imports retain write tracking through separate aliases; private host imports that lose tracking and copied or mirrored HTILE uploads remain unsupported. Clear-word encoding and console equivalence remain unmeasured on PS5 hardware. - [Native depth allocation retirement](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) covers sampling-eligible multisampled owners only. Successful release notifications must cumulatively cover both complete depth and HTILE ranges; protection changes, failed releases and partial coverage keep the original guards. Retired owners leave lookup and metadata routing, while held views and recorded work retain their images. Retirement of single-sample or sampling-ineligible depth attachments and console equivalence remain unimplemented or unmeasured. The regression uses real allocation-registry operations with a no-op host-release callback, so it does not validate the host unmap backend. - [Native MSAA depth attachment feedback](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) is not implemented. A draw sampling a native multisampled depth view while attaching its source image throws before pipeline creation or render-pass recording, including read-only attachment feedback. Sampling that image in a separate draw or dispatch remains supported. The refusal compares the held native image views and their device and attachment identity; single-sample depth behavior is retained. -- [Native multisampled depth sampling](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) accepts numeric D16 or D32, two/four/eight samples, one Z_X slice and mip, and pipe-aligned ITERATE_256 HTILE without stencil or VRS. Sampling eligibility is recorded before the attachment's first host clear: every HTILE word must prove the same initial clear of zero or one, and the complete sample-aware depth and HTILE ranges must be accessible and write-watched. Unsupported initial state or device sampling support retains attachment rendering but sampling throws; eligibility is never retried. Sampling rejects later guest writes, queued external GPU writes, and unproven HTILE clear/fill transitions. Guest publication, arbitrary guest depth import, stencil/raw-bit sampling, arrays and storage access remain unimplemented. The block footprint follows main's AMD Addrlib thin-block rule; the accepted HTILE clear words and PS5 equivalence are not measured on console hardware. PPSA99902's historical resident eight-sample D32 reader is documented in [#419](https://github.com/boykopovar/AnyPS5/pull/419#issuecomment-6105597729). +- [Native multisampled depth sampling](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) accepts numeric D16 or D32, two/four/eight samples, one Z_X slice and mip, and pipe-aligned HTILE without stencil or VRS, with ITERATE_256 on or off: the initial import reads no compressed Z planes, and Mesa's HTILE clear words (`radv_get_htile_fast_clear_value`) and Addrlib's HTILE footprint do not depend on ITERATE_256, which `ac_descriptors.c` ties to TC reads of compressed MSAA HTILE. Sampling eligibility is recorded before the attachment's first host clear: every HTILE word must prove the same initial clear of zero or one, and the complete sample-aware depth and HTILE ranges must be accessible and write-watched. Unsupported initial state or device sampling support retains attachment rendering but sampling throws; eligibility is never retried. Sampling rejects later guest writes, queued external GPU writes, and unproven HTILE clear/fill transitions. Guest publication, arbitrary guest depth import, stencil/raw-bit sampling, arrays and storage access remain unimplemented. The block footprint follows main's AMD Addrlib thin-block rule; the accepted HTILE clear words and PS5 equivalence are not measured on console hardware. PPSA99902's historical resident eight-sample D32 reader is documented in [#419](https://github.com/boykopovar/AnyPS5/pull/419#issuecomment-6105597729). - [sceAgcDriverSetSubmitValidationMode, sceAgcDriverGetSubmitValidationMode, sceAgcDriverSetSubmitValidationConfig, sceAgcDriverGetSubmitValidationConfig, sceAgcDriverSetValidationErrorOutputFrequency, sceAgcDriverGetShaderDebuggingStatus](../../../core/libs/prx/libSceAgcDriver/Unimplemented.cpp) (libSceAgcDriver) - the five validation calls return `0x8A6C1000` without reading their arguments or writing caller memory, and `sceAgcDriverGetShaderDebuggingStatus` returns 1, as [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L250-L291) reads them from Sony's libSceAgcDriver.sprx: each validation export is `mov eax, 0x8a6c1000; ret` in the copy Dragon Quest VII Reimagined bundles (0x7bb0-0x7bf0), and the debugging status is 1 in the firmware 5.50 library (0x6740). The parameters (a 32-bit mode or frequency, a mode output, a configuration pointer) follow OpenAGC and are never read. Whether later retail firmware enables submit validation or shader debugging is unverified; no title in this repository is known to call them +- [sceAgcDriver retail constant results](../../../core/libs/prx/libSceAgcDriver/Unimplemented.cpp) (libSceAgcDriver) - the debug, capture, GPU printf, register access, GPU reset, suspend point and user data exports taken from the retail `libSceAgcDriver.sprx`, and `sceAgcDriverRegisterOwnerForSystem`, `sceAgcDriverInitResourceRegistrationInterface` and `sceAgcDriverSetResourceRegistrationInfo` in [Registration.cpp](../../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp), are declared without parameters: the console bodies return a constant without reading any argument, so their signatures are unknown. `sceAgcDriverAllocateToolMemoryForGpuReset` and `sceAgcDriverGetAllocatedToolMemoryForGpuReset` read only the output pointer (fourth and second argument, a 12-byte range the console zeroes); the other arguments are declared as `uint64_t`. - [Indices far past every vertex fetch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - when a draw's highest index is at least 64Ki vertices past the largest per-vertex record count, every index past that count is moved to it in a copy of the indices, so the zero-padded vertex copies are not sized to the original index (PPSA04159 draws a list of `0xffffffff` with primitive restart off, which asked for 160 GiB). Each attribute fetched by vertex index reads zeros there as at the original index (out-of-bounds select 0 or 1); a draw with another out-of-bounds select keeps its indices. The vertex index the shader sees changes for the moved indices, which a shader that reads it directly would notice - [Depth clear draws](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - a draw with `DB_RENDER_CONTROL.DEPTH_CLEAR_ENABLE` is taken to write `DB_DEPTH_CLEAR` to every pixel it covers whatever the depth test, depth bounds and depth bias say: it is drawn with an always-passing depth write and the viewport depth range pinned to the clear value. The draw is rasterized as submitted, so its coverage is the draw's own. Whether the DB still applies the depth test or HiZ to such a draw is not measured - [GS_FAST_LAUNCH geometry](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `VGT_SHADER_STAGES_EN.GS_FAST_LAUNCH = 1` with a point list and `GE_CNTL` groups of one vertex and one primitive decodes as one point per subgroup, with the primitive limit taken from `GE_NGG_SUBGRP_CNTL.PRIM_AMP_FACTOR` and the GS/ES VGPR counts not required. Follows how RADV drives mesh shaders on gfx10.3 (`ac_nir_lower_ngg.c`, the workgroup index comes from the vertex id of the first thread), not measured on the console. Fast launch 2, other group sizes or input primitives and indexed fast-launch draws still throw. PPSA03671's fast-launch draws decode, but the title stops earlier in a wave64 vertex program, so they have not reached the GPU yet. @@ -31,9 +36,10 @@ - [sceAgcDriverGetEqContextId](../../../core/libs/prx/libSceAgcDriver/Eq/src/Query.cpp) (libSceAgcDriver) - returns `ident`: an end-of-pipe interrupt is delivered only to the registration whose id equals the raising queue, so `ident` is that context's id (titles register 0 or 1 for graphics and `0x20 + 8 * pipe + queue` for compute). The console's value is not known; KytyPS5 returns the event's `data`, which it fills with the interrupt's context id, whereas `data` here is the EV_CLEAR trigger count. Other event filters throw - [User data registers](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp) (AGC driver) - a `SPI_SHADER_USER_DATA_*` or `COMPUTE_USER_DATA_*` register the title never wrote reads as 0 when a stage's RSRC2 count covers it, as Kyty (zeroed `UserSgprInfo`) and SharpEmu (missing registers read 0) do, and as AMD PAL and Mesa initialize them; the console's value is not confirmed - [SET_PREDICATION](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (AGC driver) - boolean predication follows the AMD PM4 encoding [Mesa](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/common/sid.h) uses: operation 3 reads 64 bits and 4 reads 32 bits, and bit 8 runs predicated packets when the value is non-zero (clear: when it is zero); the bit 12 hint is ignored. The address must have its low 4 bits clear, as `sceAgcDcbSetPredication` writes it. Query predication (Z-pass, primitive count), a predicated COND_EXEC, predicated command buffer chains and flips in predicated command buffers throw +- [Barycentrics without mesh shaders](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp) (AGC driver) - a pixel shader that reads barycentrics on the mesh path is emulated only on a device with `VK_EXT_mesh_shader`; a device with neither it nor `VK_KHR_fragment_shader_barycentric` (reported on Linux RADV with a GCN 5 GPU) throws that the device has neither the barycentric extension nor mesh shaders, because a geometry stage cannot follow a mesh stage and no other route is implemented. `VK_AMD_shader_explicit_vertex_parameter`, which a GCN 5 device on RADV exposes, is an unexplored option. - [NGG vertex programs on the mesh path](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - an unregistered vertex program with `PRIMGEN_EN` that uses LDS or `s_barrier` runs on the geometry (mesh) path. Each input primitive is assumed to yield at most one output primitive, the subgroup is bounded by `GE_MAX_OUTPUT_PER_SUBGROUP` as a vertex budget, and the GS/ES VGPR count checks are skipped, because the merged inputs keep their positions. Taken from the code Mesa's ACO emits for NGG culling and from PPSA59322 and PPSA99010 draws, not measured on hardware. Registered programs keep the vertex path - [VGT_SHADER_STAGES_EN 0x02002008](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - this one value, `ES_EN` = 1 without `HS_EN` on a non-patch draw, is drawn as the vertex path. AMD's CIK register reference calls `ES_EN` = 1 `ES_STAGE_DS` (the ES stage runs the tessellation evaluation shader), which does not explain it on a non-patch draw. Seen only on PPSA29413 and not verified on the console; other vertex-path values with `ES_EN` set still throw. -- [Strided buffer overhang](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a strided buffer descriptor that ends less than one stride past the end of the readable registered allocation holding its base is bound only up to that allocation's end, even when another readable allocation starts right there. This assumes the bytes past the end are the rounded-up `num_records` of the last record (RADV rounds it up), which no fetch inside the buffer reads; a guest that really keeps a vertex split across two adjacent allocations would read zeros for the cut tail. Taken from the RetroArch Dolphin core case in #2912, not measured on hardware. +- [Strided buffer overhang](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a strided buffer descriptor that ends less than one stride past the end of the readable registered allocation holding its base is bound only up to that allocation's end, even when another readable allocation starts right there. This assumes the bytes past the end are the rounded-up `num_records` of the last record (RADV rounds it up), which no fetch inside the buffer reads; a guest that really keeps a vertex split across two adjacent allocations would read zeros for the cut tail. The same holds for a read-only strided buffer that starts below the render target and runs less than one stride into it: it is bound only up to the target, as a vertex fetch whose attribute descriptor starts at the attribute's offset reaches that far past the attribute's last bytes. Taken from the RetroArch Dolphin core case in #2912, not measured on hardware. - [Buffer descriptor CACHE_SWIZZLE hint](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - bit 30 of word 1 in a buffer resource descriptor (`words[1] & 0x40000000u`) is normalized to zero when binding guest buffers in `ShaderResources::addGuestBuffer`. On GFX10 (gfx1013), bit 30 of V# word 1 is `CACHE_SWIZZLE` (Mesa's `gfx10-rsrc.json`: STRIDE [29:16], CACHE_SWIZZLE [30], SWIZZLE_ENABLE [31]), a cache-bank hint with no effect on the address or data, which is dropped here. - [Buffer descriptors past the range limit](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a buffer V# whose size is over `maxStorageBufferRange` is bound only up to the end of the readable registered allocation holding its base when that allocation ends before the limit, and as the empty placeholder buffer, like a null V#, when no registered allocation holds its base; a V# over an allocation that reaches the limit still throws. Titles leave such words in descriptor slots their shaders do not reach on the path taken (PPSA10528 binds `c8010002 2c6bec20 c0053c00 04000013`, about 33 TB from an address above the user address space). What the console returns for an access past the bound range is not known; here the device's robust buffer access decides. Follows the review of #3202, not measured on hardware. - [Vertex fetch overhang](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - a zero-padded vertex fetch whose valid range (`num_records * stride`) ends less than one stride past the end of the readable memory at its base copies only the readable bytes and reads the rest as zeros, instead of failing the draw; a longer overhang still fails. This extends the strided buffer overhang rule above (the last record is the rounded-up `num_records`) to the staging copy and is not measured on hardware. The observed case is a fetch of 14858 records of 44 bytes that ends 8 bytes past the end of the 0xa0000-byte mapping holding it. @@ -43,7 +49,7 @@ - [Shader headers](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - registration and ABI resolution read fixed header fields through aligned copies, so the header pointer itself may have any alignment. PPSA26344 passes headers 4 and 1 bytes past an 8-byte boundary, and PPSA01317 passes 4-byte-aligned headers to ABI resolution; console alignment requirements are unknown - [Registered shader types](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - types 0, 1, 2, 4, 6 and 7 are prepared and type 5 is skipped. Any other type, such as type 8, is logged (`unsupported registered shader type 8; it is not prepared at registration`) and left unprepared at registration, and throws when it is used later. Hello Neighbor 2 (PPSA07426), Ad Infinitum (PPSA10600) and Ghostrunner (PPSA03685) register type 8 headers and run on past them; what a type 8 header describes is unknown, #1892 found no context registers in it. - [NGG vertex workgroups](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - type-2 PRIMGEN programs using DS instructions or `s_barrier` are prepared with the draw's routing and primitive registers on the existing mesh path. The workgroup requirement follows [Mesa's NGG lowering](https://github.com/mihawk-99/PS5_Mesa/blob/b3588f78dd7fd46fbadc7ecd487e5f14d4213e4b/src/amd/common/nir/ac_nir_lower_ngg.c#L1135); the mesh path supplies LDS and two guest lanes per host invocation for wave64 on a 32-lane host subgroup. Console routing has not been measured. Passthrough and tessellation keep their existing paths; unsupported mesh assembly and subgroup budgets throw. -- [Shader header copies](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a header bound for a draw or dispatch at another address than the registered one is accepted when its fixed fields match the registered header except `user_data`; the registered header's metadata is used. PPSA21567 binds such a copy of a vertex shader header with `user_data` set to null. Whether the console reads the copy's `user_data` is unknown +- [Shader header copies](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a header bound for a draw or dispatch at another address than the registered one is accepted when its fixed fields match the registered header, and every array it points at still holds the registered bytes; the registered header's metadata is used. `user_data` is not compared (PPSA21567 binds such a copy of a vertex shader header with `user_data` set to null; whether the console reads the copy's `user_data` is unknown). A copy whose registers, context registers, specials or semantics differ, or whose arrays are unreadable, throws `... refers to a replaced shader header`; what the console does with a header a title patches while relocating it is not measured. PPSA01325 (#2914) resolves the ABI with such a copy - [HtileFillCovers](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - a fill is taken to cover a depth surface's whole HTILE when it spans 32 KiB per 1024x512 pixel block from DB_HTILE_DATA_BASE, the pipe-aligned SW_64KB_Z_X metadata extent AMD PAL's gfx10 addrlib gives for GB_ADDR_CONFIG 0x00100044 (1-8 samples, D16/D32); other metadata layouts are not decoded. A partial fill is ignored (the surface keeps its contents); not measured on the console. - [Depth fast clear through HTILE](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - a surface cleared by an HTILE fill is cleared to DB_DEPTH_CLEAR, also when it is next sampled. For a depth-only HTILE the clear word also carries a value (radv writes `minz = maxz = round(depth * 0x3fff)`, `HTILE_Z_CLEAR_REG` in Mesa's `src/amd/common/ac_descriptors.h`, and allows TC-compatible fast clears only to 0.0 or 1.0); which of the two the texture unit returns for a cleared tile was not measured. - [Depth-only and stencil HTILE fill](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - in a whole-HTILE fill, ZMask (bits 0-3) 0 clears the depth and SMem (bits 8-9) 0 clears the stencil, while SMem 3 leaves the stencil alone (`HtileFillClears`); other SMem values are taken to leave it alone too. The bit meanings follow the gfx10 HTILE layout as the code reads it, not a measurement; `agc_driver_depth_fast_clear_submit` pins them for the paths the Driver takes. @@ -55,19 +61,22 @@ ### Functional - [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. +- [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 (at least the sampled textures alive) 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 and never falls below 2 GiB. The storage budget keeps no room for the part of that 2 GiB floor the sampled textures alive do not use: it gives room back as they grow, at its next read. As the heap's other use grows the sampled budget reaches its floor first, then the storage budget falls. 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, sampled textures included, can still exceed the reported budget. The eighth is the sampled formula's value reused and the 512 MiB floor and the caps the earlier ones; none was measured beyond PPSA11386 on one 10 GB GPU and PPSA16064 on one 8 GB GPU, and a title whose storage working set exceeds the shrunk budget re-creates storage images at each use. - [Polygon primitive type](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `VGT_PRIMITIVE_TYPE` 21 (`PrimitiveType::Polygon`) decodes to `VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN` and is treated as inactive for vertex point size, like triangle fans and strips. PPSA01317 (Gran Turismo 7) submits polygon draws; whether the console performs polygon-mode decomposition or custom vertex routing is unverified. -- [Native multisampled DCC fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) import uniform 0000/0001/1110/1111 keys into a single-layer, level-zero SW_64KB_R_X color image with 2, 4 or 8 fragments. Color and DCC/CMASK ranges must stay write-watched and unchanged; expanded or mixed initial DCC, register CMASK clears, compressed payloads, color transforms, lossy modes, metadata changes, guest publication and writes over a live owner throw. Driver-mediated reads of the color and metadata stay guarded. The metadata extent follows [AMD Addrlib](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp#L347-L454) with the [PS5 Mesa model](https://github.com/mihawk-99/PS5_Mesa/blob/0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8/src/amd/vulkan/winsys/ps5/radv_ps5_winsys.c): GFX10.1, 16 pipes, 256-byte interleave, two maximum compressed fragments. Console geometry, concurrent guest mutation and transitions outside this subset are unverified. +- [Native multisampled DCC fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) 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. +- [Multisampled DCC comp-to-single clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (AGC driver) - RADV's multisampled comp-to-single clear dispatch ([`radv_clear_dcc_comp_to_single`](https://github.com/mihawk-99/PS5_Mesa/blob/1ba9874f478657ccf6da72ce55f0260164cc0bd0/src/amd/vulkan/meta/radv_meta_clear.c#L954)) is recognized by its code and not run. Its clear color (user data 5 to 8, which the shader stores as the four dwords of sample 0's texel) is recorded for the surface its image descriptor names, and DCC keys of 0x10 on a multisampled target of that surface, pipe-bank XOR, size and sample count import as a clear to it, for texels of 8 to 128 bits. A record is dropped when an unmap or allocation removal covers its surface's base. The guest's color memory doesn't get the color at the blocks' first pixels, and the recorded color is assumed to be the one the keys refer to: RADV writes 0x10 keys only in a fast clear that runs this dispatch for the same image right after, and initializes DCC to 0x00 or 0xff on an image's first use ([`radv_init_color_image_metadata`](https://github.com/mihawk-99/PS5_Mesa/blob/1ba9874f478657ccf6da72ce55f0260164cc0bd0/src/amd/vulkan/radv_cmd_buffer.c#L16894)). A new image on memory reused within one mapping, with the same address, XOR, size and sample count, whose clear runs a comp-to-single shader other than the recognized one, would still import the old color. - [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. - [Collapsed color array views](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) preserve the selected slice in the XOR address equation when the view is represented as a shifted 2D surface. Native Vulkan coverage checks R_X draws, array reads, publication and re-upload for slices 1-3; Lost Soul Aside PPSA26358 validates the RGBA16F cube mip chain on Windows/NVIDIA. Other devices and console comparisons remain unverified. Full-array views retain their layer addressing. -- A [depth surface](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) is retired when a color target or a written storage image it does not serve overlaps it, or when it is sampled through a view it cannot serve and guest memory over it was written since it was last bound; lookups then skip it, so sampling reads the memory like any texture. Bound again, it keeps its own depth instead of the bytes written over it. Without write watching, a view it cannot serve still throws. A retired surface also gives up its depth and stencil planes, so another surface sharing them no longer copies from it. +- A [depth surface](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) is retired when a color target or a written storage image it does not serve overlaps it, or when it is sampled through a view it cannot serve and guest memory over it was written since it was last bound; lookups then skip it, so sampling reads the memory like any texture. Bound again, it keeps its own depth instead of the bytes written over it, except for the depth planes the next entry keeps in guest memory. Without write watching, a view it cannot serve still throws. A retired surface also gives up its depth and stencil planes, so another surface sharing them no longer copies from it. +- [Depth planes in guest memory](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): only single-sample `D32_SFLOAT` and `D32_SFLOAT_S8_UINT` planes with the 64 KiB Z swizzle (`SW_MODE` 24) reach guest memory; D16, other swizzles, stencil and multisampled planes keep depth in the host image only. A plane drawn since its last store is stored, uncompressed, when a buffer upload, a `GuestBuffers` V#, an image lookup or a pending-store flush overlaps it; CPU reads and reads through address-based heaps are not caught. Guest writes are ordered when collected (bind, flush, collect epoch), not when made, at 64 KiB granularity: a collected write reloads the whole plane unless an HTILE clear is pending. A store outside a recorder waits for the GPU. The console keeps HTILE-compressed tiles compressed in memory until a decompress (Mesa `ac_descriptors.h`). - [Depth plane copies](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): a depth plane sampled as a 2D array or a cube, or a D16 plane read as `R16_UINT`, is copied into an image of its own (`DepthPlaneCopy`), kept per base address, extent, format and slice count; a slice comes from the live depth surface when one is bound at the slice's address and from guest memory (detiled) otherwise. A draw or dispatch that binds the plane copies again only the slices that changed and records nothing when none did: a surface slice when its image or write serial differs from the one copied (the serial is bumped when the surface is created, when a draw binds it as its depth target, when a pending fast clear is applied and when a written storage view of the plane is copied back), a guest-memory slice when its bytes were written since they were last read (`GuestMemory::UnchangedSince`), a snapshot write overlaps them, or its address, size or tile mode changed. The surface of the latest depth draw is copied at every use until another depth target is bound, since that draw may still be recorded after the copy. A slice's address is the base plus a whole 64 KiB-block depth slice per layer (`DepthSliceBytes`), and the cube face order is the array order; both are inferred from the titles' use, not measured on hardware. A 2D-array T# whose `BASE_ARRAY` is past slice 0 reads slices `BASE_ARRAY` to `LAST_ARRAY` of the copy, which holds slices 0 to `LAST_ARRAY`, and a 2D binding of it reads slice `BASE_ARRAY`; that follows the colour 2D-array semantics and the titles that hit it (PPSA01556, PPSA03912), and was not measured on hardware for depth surfaces. Planes with mips, a cube or a 2D T# with a base slice other than 0, a `BASE_ARRAY` past `LAST_ARRAY`, a cube with other than 1 or 6 slices, and other views of the planes still throw, and so do these copies of a 16-bit plane with stencil that the device stores as D24 or D32 (16-bit depth with stencil, below). - [Repacked format-43 copies](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver): a sampled texture of format 43 (10-bit R, 11-bit G and B unsigned floats from bit 0) is uploaded into a `B10G11R11_UFLOAT` image with its R and B fields swapped (`RepackTextureWord`) and viewed with R and B swapped back. A texture whose results a storage image still holds is copied from it again, whole, whenever the image changed (`RefreshDepthCopy`); a 3D one is flushed to guest memory and uploaded instead. Storage reads and writes of format 43 keep the packed `R32_UINT` path. - [SW_64KB_Z_X color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp) (libSceAgcDriver) use the texture equations and 64 KiB block geometry of `SW_64KB_Z_X`, whose addresses are checked against [AMD addrlib GFX10](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp) in the texture tiling tests. Any 256-byte pipe/bank XOR below the block size is accepted, as for `SW_64KB_R_X` targets and Z_X textures; which of those bits the console honours, and the render results, are not measured. @@ -94,17 +103,21 @@ - On a device without VK_EXT_robustness2's `nullDescriptor`, such as MoltenVK, a null image descriptor in a typed heap is a [placeholder image](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PlaceholderImages.cpp) of that heap's type, 1×1 and cleared to zero, which every null descriptor of the heap shares. Reads give zeros, as `nullDescriptor` allows, but stores and atomics to it are kept, so a later read of a null storage image can return them, and a depth comparison compares against depth 0. A heap whose placeholder the device cannot create, for example 1D or 3D depth, 64-bit atomics without 64-bit image atomics, or multisampled storage without `shaderStorageImageMultisample`, throws when a null descriptor reaches it. - [Draw input snapshots](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver): `PrepareDrawBindings` caps read-only input buffer snapshots at 64 MiB (overridable via `APS5_DRAW_SNAPSHOT_MAX_MIB`). Larger inputs (streaming heaps, bindless pools) or draws exceeding the cumulative budget skip the per-draw snapshot and bind in place, avoiding multi-gigabyte CPU copy stalls (such as Ratchet & Clank: Rift Apart PPSA01474 stalling at frame ~380 copying 36 GB every 5 s). If the title rewrites those large ranges on the CPU before GPU completion, the in-place binding observes the modification, as verified by `drawSnapshotCapTests`. - [Pending storage-image results](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) are stored when something reads their memory, not when the GPU work that wrote them completes. A title that waits for that work, frees the image and reuses its memory can have the new data overwritten by the late store (#1970). A title's own CPU reads are not seen either, so a title that reads the results after waiting for a label reads stale memory. Explicit-size file writes (`write`, `pwrite`, `writev`, `pwritev`, AIO and `fwrite`) synchronize their source ranges through the [host-read hook](../../../core/libs/prx/libc/src/GuestArena.cpp); other host readers are not covered. `APS5_STORE_AT_FLIP=1` stores every pending result at each flip and `APS5_STORE_AT_LABEL=1` before each label the GPU writes; both store every pending image, not only the ones the waited-for work wrote, and neither is on by default. -- [Pending storage-image results 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, and ranges that leave the watch through a failed collect, have no saved bytes; storing such results throws. +- [`APS5_SKIP_REFUSED_DRAWS`](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp) (libSceAgcDriver) drops a draw whose graphics stage the recompiler refuses instead of failing the queue, so the frame lacks that draw, and the stage is compiled and refused again at each such draw. Compute refusals still fail. +- [Pending storage-image results over memory outside the write watch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) leave to the CPU the 64 KiB blocks whose bytes differ from the ones saved at the upload, the fill, the image copy, the previous store or the moment their range left the watch (`Unwatch`, or a host import that does not stay watched), and the blocks the watch saw the CPU write before that moment. A store made on the GPU saves its bytes through a host-visible copy that becomes the saved bytes when its batch completes; a store or a refresh that needs them earlier waits for that batch. A unit without a write generation saves the guest bytes when results land in it outside the watch. One whose range leaves the watch while its results are pending saves its bytes through the unwatch hook, which also runs when a failed collect takes a range out of the watch. A range that leaves the watch without the hook running over it (an import fallback that unwatched without saving) leaves its pending results without saved bytes; storing such results throws. A unit whose results were stored while its blocks were watched keeps only a generation; when its blocks leave the watch while nothing is pending, the hook saves nothing, and results that land in it afterwards have neither saved bytes nor a watch, so storing them throws too. - 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 - [Sparse dma-buf imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): on Linux a shared-backed range larger than one 64 MiB udmabuf buffer is imported as one sparse buffer bound to the 64 MiB buffers, on a queue of its own when the device has a second family that binds sparse memory. The path is decided before anything is imported, and a failure after that throws. A range whose offset in the backing file is not a multiple of 64 KiB starts its sparse buffer at the 64 KiB block below it (`HostImport::lead`). A range takes the single udmabuf import instead, which udmabuf's `size_limit_mb` refuses above 64 MiB by default so the range is copied, when the buffer's sparse block size is larger than 64 KiB (it is 64 KiB on RADV), when the sparse buffer does not accept every memory type a chunk import can have, when the backing file ends inside its last chunk, or when it is imported at map time on a device with no second family (the bind then needs the GPU mutex); the NVIDIA proprietary driver imports no dma-buf memory - [Unnormalized S# coordinates](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) - `FORCE_UNNORMALIZED` uses a Vulkan `unnormalizedCoordinates` sampler, so the S# LOD range, LOD bias and mip filter are not applied, which is exact only on a one-level view. - An [unnormalized S#](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) maps clamp mode 0 (repeat) on X and Y to clamp-to-edge as required by Vulkan unnormalized coordinates, and assigns the magnification filter when minification and magnification filters differ (since unnormalized coordinates sample at LOD 0). Anisotropy, TRUNC_COORD, MC_COORD_TRUNC and other clamp modes still throw. Secondary LOD bias (LOD_BIAS_SEC, bits [19:14]) is accepted and ignored, matching RDNA2 hardware measurements across explicit and implicit samples. High on Life (PPSA15388, #4736) uses secondary LOD bias and unnormalized coordinates with clamp mode 0 and differing min/mag filters during home base and city rendering. +- [TRUNC_COORD](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) is not applied: the texel a point sample reads and the bilinear weights follow the host sampler's rounding. On RDNA 2 (RX 6700 XT), an S# with TRUNC_COORD 0 rounds the exact scaled coordinate to 8 fractional bits, ties to even, and one with TRUNC_COORD 1 truncates it, for point and bilinear filters. AMD's Windows driver (32.0.21045.5002) truncates for nearest samplers and rounds for linear ones. Under it, a TRUNC_COORD 0 point sample at most 1/512 texel below a texel edge reads the texel below the edge where the console reads the one above, and `k / width` computed in f32 lands there for about half of all k. A TRUNC_COORD 1 bilinear weight can be 1/256 above the console's. Other host drivers are not measured. - [Background link-time-optimized relink of library pipelines](../../../core/libs/prx/libSceAgcDriver/Execution/include/PipelineLinkOptimization.hpp) (libSceAgcDriver) is skipped on every GPU under NVIDIA's proprietary Vulkan driver (`VK_DRIVER_ID_NVIDIA_PROPRIETARY`), so those pipelines keep their fast-linked form. The optimized link loses the device when bound on an RTX 3080 (Ampere, #2866) and an RTX 4060 (Ada, #3434); an RTX 5060 and an RTX 5080 (Blackwell) on the same driver do not, so the scope is wider than the evidence. No standard Vulkan query tells the architecture apart, and the driver version that fixes it is unknown. `APS5_NO_PIPELINE_LTO` disables the relink on every driver. - [8_SRGB and 8_8_SRGB textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp) (libSceAgcDriver) that the device samples as R8_SRGB or R8G8_SRGB use its own decode, which need not match gfx1035: NVIDIA's R8_SRGB is one bf16 step off in 29 of 256 codes. A device that cannot sample them gets an RGBA8 sRGB copy instead (a blit of the R8 or RG8 texels, refreshed with the storage image's version), whose decode is likewise the device's; 3D textures and storage images that are not single-depth and format-matched are copied from their snapshot instead. - [PM4 submission rings](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/SubmissionRing.cpp) (libSceAgcDriver) keep only the first 512 dwords of each submission, so the packets past them are not printed. The [failure report](../../../core/libs/prx/libc/src/FailureReport.cpp) dump does not allocate from the C runtime heap: packet names come from static strings, each line is formatted with integer and string conversions into a 512-byte stack buffer and written with one `WriteFile`. It still runs on whatever state the crash left. It waits about 100 yields for each ring's lock and otherwise prints that queue as busy, so a crash while recording a submission loses that queue's ring. It reads the rings without checking them, so a crash that corrupted them can fault inside the dump; such a fault during the report of an unhandled exception stops in that report's `Sleep(INFINITE)` and the process hangs instead of ending. The reports run at most once per process. Linux has no crash report: there the rings are printed only by `APS5_STALL_WATCHDOG_SECS`. - [WAIT_REG_MEM](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp) reports a diagnostic after `APS5_GPU_WAIT_TIMEOUT_MS` without treating the timeout as completion. An unsatisfied wait keeps polling and checking driver failures; a missing producer can therefore leave its queue waiting indefinitely. Existing recorded-label shortcuts remain in use. +- A [WAIT_REG_MEM](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp) (libSceAgcDriver) on a queue 0x20 label can time out while the awaited word holds the address of another slot of the same label area instead of 0 or 1, with no store to that label in the PM4 ring window (`APS5_PM4_RING`). What writes the address (guest CPU or the emulator) and whether the console would have satisfied the wait before the slot was reused are unverified; the wait keeps polling and its queue stays blocked. Seen in several titles on a Windows NVIDIA GPU; not measured on hardware. +- [PM4 type-0 packets](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/CommandValidation.cpp) (libSceAgcDriver) - a dword whose packet type is 0, a zero header word included, is rejected with `unsupported PM4 packet type`, so the submission throws. Whether the console's command processor skips or executes such dwords, and what a run of them in front of a REWIND packet means, is unverified; shadPS4 (`liverpool.cpp`) and KytyPS5 (`pm4.cpp`) also treat type 0 as unsupported. One such submission begins with a seven-dword NOP and an eight-dword ACQUIRE_MEM, then 169 dwords of zero headers (dwords 15 to 183, zero in one run and stale words in another), and its packets start at dword 184, as sections of NUM_INSTANCES, SET_BASE, REWIND, NOP and ACQUIRE_MEM separated by zero gaps of 26 to 455 dwords (odd lengths included); letting a zero header through only moved the stop to a stale word. - [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. @@ -112,42 +125,45 @@ - A [label or store](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (`RELEASE_MEM`, `WRITE_DATA`, `DMA_DATA`) into guest memory the device has no host import of waits for all recorded work and is then written by the CPU, so the queue worker stalls once per such label. The console's CP stores it in stream order without that stall. - [WRITE_DATA to thread-trace user-data registers](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver) updates the queue's UCONFIG values for register destinations `0xc340..0xc347`, including repeated writes to one address; these are not guest-memory labels. The range follows AMD's GFX10 `SQ_THREAD_TRACE_USERDATA_0..7` definitions in [the Linux register header](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/include/asic_reg/gc/gc_10_1_0_offset.h) and the packets emitted by PPSA21564's bundled AGC driver before its EOP flip. Other register destinations throw. GPU thread-trace capture and its user-data stream are not emitted; only the final register state is retained. Console trace behavior is unverified. - [WRITE_DATA with DST_SEL 3](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver) writes guest memory like DST_SEL 5. AMD's GFX10 header names 3 the GDS, but Killzone: Liberation (PPSA11142) writes its frame fence with `sceAgcDcbWriteData(dst 6, cache policy 3, write confirm)`, which the SPRX-confirmed encoding ([OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/cb_builders.c)) turns into DST_SEL 3 with a 40-bit label address in the title's own memory that `RELEASE_MEM` and `WAIT_REG_MEM` then use; the console must write that label or the title would wait on it forever. Whether the console routes it through the GDS or a cache is unknown -- [SET_*_REG_INDIRECT](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) reads its register list when the command buffer is submitted, with the packets. The console's graphics PFP reads it when it reaches the packet, so a list written after the submission (by the CPU, or by GPU work ahead of the packet behind PFP_SYNC_ME) is read here before that write. +- [SET_*_REG_INDIRECT](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) reads its register list when the command buffer is submitted, with the packets. The console's graphics PFP reads it when it reaches the packet, so a list written after the submission (by the CPU, or by GPU work ahead of the packet behind PFP_SYNC_ME) is read here before that write. When the list read at submission holds an offset dword that is not a register offset (bits 16-27 set, or the 0xffffffff sentinel), the worker reads the list again where the packet executes and writes that copy. PPSA23566 (UFC 6) intermittently submits a SET_CONTEXT_REG_INDIRECT whose first entry is 0x038f038f or 0x03900390 in both copies; what the PFP does with such an entry is unknown, so a copy that still holds one throws unless `APS5_SKIP_INVALID_INDIRECT_REGISTERS=1`, which skips its invalid entries and writes its valid ones. - The [shader float mode](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (`FLOAT_MODE`, `DX10_CLAMP`, `IEEE_MODE` and `FP16_OVFL` of `COMPUTE_PGM_RSRC1` and `SPI_SHADER_PGM_RSRC1_PS/GS/HS`, bit positions from Mesa's `gfx10.json` and `gfx103.json`) is decoded into `GuestContext::floatMode` and is part of the shader cache keys and the serialized request (#981). `IEEE_MODE` is read by `v_min_f32`, `v_max_f32`, `v_min3_f32`, `v_max3_f32` and `v_med3_f32`, their f16 forms, `v_min_f64` and `v_max_f64`, `IEEE_MODE` and the `FLOAT_MODE` output denormal bits by the output modifier, `DX10_CLAMP` by the clamp modifier, `FP16_OVFL` by every instruction with an f16 result (see its entry), and the whole mode by the 16-bit interpolation instructions (see their entries). With IEEE=1 a signaling NaN operand returns that NaN quieted (the first operand checked first, min3/max3 as two nested pairs, med3 as min3 when any operand is NaN), measured on RDNA2 (gfx1035) over every combination of ±0, ±1, ±inf, denormals, quiet and signaling NaNs with f32 denormals kept and flushed. IEEE=0 and unknown modes keep the previous f32 lowering, which matches IEEE=0. The f16 forms choose their NaN results on the bits: a NaN operand returns the other operand unchanged (src0 when both are NaN), and with IEEE=1 a signaling NaN operand returns that NaN quieted, in the same order as f32. `v_min_f64` and `v_max_f64` quiet a signaling NaN operand the same way with IEEE=1; with IEEE=0 they return src0 unchanged when src1 is NaN, otherwise src1 when src0 is NaN. Both were measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1 with f32 denormals kept and flushed and f16/f64 denormals kept; RDNA2 has no f64 min3, max3 or med3. Unknown modes follow IEEE=0. `v_min_f16` and `v_max_f16` flush each denormal input to its signed zero when bit 0 of the f16/f64 denormal field is clear (0 and 2); fields 1 and 3 preserve inputs, even when output flushing is enabled. Measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1, including signed zeros, NaNs and neg/abs modifiers. Unknown modes keep f16 denormals. `v_min_f64` and `v_max_f64` flush subnormal inputs to signed zero when bit 0 of the f16/f64 denormal field is clear (0 and 2); fields 1 and 3, and an unknown mode, preserve inputs. The fully flushed and preserved f64 behavior was [measured on gfx1036](https://github.com/boykopovar/AnyPS5/pull/2036); split-mode selection follows [LLVM's denormal-mode enumeration](https://llvm.org/docs/AMDGPUUsage.html#floating-point-denorm-mode-enumeration-values). Other f64 arithmetic does not model these denormal controls. NaN quieting in f16 arithmetic (the unary instructions and the f32 arithmetic follow it, see below) and `FLOAT_MODE` (f32 denormals flushed before the min/max compare in Astro's Playroom's 0xc0) still follow a single mode, as the entries below record. The mode comes from the registered shader header's `sh_registers`, the same source at registration and at draw/dispatch so the prepared artifact keys match. A title that rewrites RSRC1 at run time, or a raw compute program with no header, compiles with that mode or none. - A [WAIT_REG_MEM](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp) (libSceAgcDriver) that a compute queue passes on another queue's recorded label store, before the GPU has executed it, is landed before the queue's next dispatch, draw, COND_EXEC or predicated packet reads guest memory: the worker polls until memory satisfies the wait or no unfinished batch writes the label. The graphics queue (queue 0) is exempt, as landing its waits costs PPSA01341 about a quarter of its frame rate; a title that fills memory a graphics command buffer reads after it submits the buffer behind such a wait reads it early there. `APS5_LAND_WAITS_ALL_QUEUES=1` lands the graphics queue's waits too. - [CPU reads behind an address-based build](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp) (libSceAgcDriver) - an address-based store (FLAT/GLOBAL, or through a descriptor the recompiler cannot resolve to a static buffer) is not recorded as a pending write: its batch holds a lease on the registered allocations instead, and the flush hook does not wait for it. A DISPATCH_INDIRECT whose arguments are read on the CPU first finishes the batches recorded with a lease (`Graphics::FinishLeaseWork`). These reads do not, so they can see guest memory from before such a build ran: the indirect draw arguments and draw count of a draw resolved on the CPU (`Pm4::ReadDrawArguments`, `Pm4::ReadDrawCount`), the count record of the [copy kernel](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp) HLE (guarded only by its shape check), and the checks in [Submission.cpp](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) that decide whether a queue awaits a label only the title writes (`awaitsTitle`, `orderReleased`). No test covers them and no title has been seen to read such memory there. - The [dispatches that follow a compute queue's wait](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp) (libSceAgcDriver), up to the next wait that memory does not satisfy, a packet that writes guest memory other than a label, or a conditional packet, are resolved (memory captured, shader recompiled) when the wait passes, as the CP reads them right after it; the graphics queue is exempt (`APS5_GROUP_CAPTURE_ALL_QUEUES=1` includes it). The device still reads guest memory when the dispatch is issued; when that memory no longer matches the capture, the dispatch is resolved again from current memory. -- [PA_CL_CLIP_CNTL](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - `DX_LINEAR_ATTR_CLIP_ENA` (bit 24) clear is drawn like bit 24 set, with Vulkan's clipping of `NoPerspective` outputs; what a clear bit changes on the console is undocumented. +- [PA_CL_CLIP_CNTL](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - `CLIP_DISABLE` (bit 16) and `DX_LINEAR_ATTR_CLIP_ENA` (bit 24) are accepted without changing pipeline state: Vulkan clips primitives against viewport guard bands and clips `NoPerspective` outputs linearly; what clearing bit 24 or setting bit 16 changes on the console beyond standard clipping is undocumented. - [10_11_11 unorm color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R32_UINT attachments: the pixel shader packs each export, rounded to nearest even like format-30 image stores, which is assumed to match the CB's conversion. - [11_11_10 float color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R32_UINT attachments: the pixel shader packs each export toward zero like format-43 image stores (overflow to the largest finite value, negatives to zero, NaN kept), which is assumed to match the CB's conversion; only the reversed component order (COMP_SWAP 2) is accepted. -- [PA_SU_SC_MODE_CNTL.PROVOKING_VTX_LAST](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp) (libSceAgcDriver) uses Vulkan's last-vertex mode for native primitives. Bit 19 follows [opengnm's `flatshade_first` mapping](https://github.com/blackbearreloaded/ps5-opengl/blob/db752da4c89380af228497b5b6852163a4e61069/src/gallium/ps5/ps5_screen.c#L2244); vertex selection follows [Vulkan](https://docs.vulkan.org/spec/latest/chapters/drawing.html#drawing-primitive-topologies), without console measurements. A [rect-list](../../../core/shader/recompiler/SpirvBackend/src/RectListShaders.cpp) draw in last mode gives all four corners of each rectangle the flat inputs of its third vertex through the generated control stage, as first mode gives them those of its first, so Vulkan's choice of provoking vertex for the tessellated triangles does not matter; which vertex the console takes for a rectangle is not measured either. A geometry program's primitives keep the vertex indices of its own primitive export on the [mesh path](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp), so last mode takes their flat inputs (from [Vulkan](https://docs.vulkan.org/spec/latest/chapters/vertexpostproc.html#vertexpostproc-flatshading)) and their [layer](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) from the last index of each primitive, while their per-vertex inputs keep the order of first mode ([Vulkan](https://docs.vulkan.org/spec/latest/chapters/primsrast.html#primsrast-barycentric)); that the console does the same for an exported primitive is not measured. Last mode throws without `VK_EXT_provoking_vertex`, for tessellation primitives, for triangle strips or fans assembled for a geometry program (whose odd-triangle order on the console is not measured), explicit per-vertex flat inputs, and triangle strips whose pixel shader reads a vertex slot itself (`v_interp_mov_f32`, or 16-bit interpolation, which is not checked yet), since the vertex order of odd strip triangles [depends on the device](https://docs.vulkan.org/refpages/latest/refpages/source/VkPhysicalDeviceFragmentShaderBarycentricPropertiesKHR.html). `v_interp_p1_f32` and `v_interp_p2_f32` read P10 and P20 against the guest's I and J, which come from `gl_BaryCoordKHR` in the same vertex order as the per-vertex values, so the order cancels out and strips with only those reads draw. A FLAT_SHADE input without pass-through that is read through `v_interp_mov_f32` P10 and not P20 [returns the provoking vertex's value](../../../core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp), like P0. ps5-opengl reads every flat input that way, and its driver passes the GL 4.6 CTS on a PS5 (#1413). The lane itself was not measured on the console, so with the first provoking vertex such a shader throws. +- [PA_SU_SC_MODE_CNTL.PROVOKING_VTX_LAST](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp) (libSceAgcDriver) uses Vulkan's last-vertex mode for native primitives. Bit 19 follows [opengnm's `flatshade_first` mapping](https://github.com/blackbearreloaded/ps5-opengl/blob/db752da4c89380af228497b5b6852163a4e61069/src/gallium/ps5/ps5_screen.c#L2244); vertex selection follows [Vulkan](https://docs.vulkan.org/spec/latest/chapters/drawing.html#drawing-primitive-topologies), without console measurements. A [rect-list](../../../core/shader/recompiler/SpirvBackend/src/RectListShaders.cpp) draw in last mode gives all four corners of each rectangle the flat inputs of its third vertex through the generated control stage, as first mode gives them those of its first, so Vulkan's choice of provoking vertex for the tessellated triangles does not matter; which vertex the console takes for a rectangle is not measured either. A geometry program's primitives keep the vertex indices of its own primitive export on the [mesh path](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp), so last mode takes their flat inputs (from [Vulkan](https://docs.vulkan.org/spec/latest/chapters/vertexpostproc.html#vertexpostproc-flatshading)) and their [layer](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) from the last index of each primitive, while their per-vertex inputs keep the order of first mode ([Vulkan](https://docs.vulkan.org/spec/latest/chapters/primsrast.html#primsrast-barycentric)); that the console does the same for an exported primitive is not measured. Last mode throws without `VK_EXT_provoking_vertex`, except for the [generated barycentric tessellation stages](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp), which write the last vertex's flat values at every corner, for tessellation primitives, for triangle strips or fans assembled for a geometry program (whose odd-triangle order on the console is not measured), explicit per-vertex flat inputs, and triangle strips whose pixel shader reads a vertex slot itself (`v_interp_mov_f32`, or 16-bit interpolation, which is not checked yet), since the vertex order of odd strip triangles [depends on the device](https://docs.vulkan.org/refpages/latest/refpages/source/VkPhysicalDeviceFragmentShaderBarycentricPropertiesKHR.html). `v_interp_p1_f32` and `v_interp_p2_f32` read P10 and P20 against the guest's I and J, which come from `gl_BaryCoordKHR` in the same vertex order as the per-vertex values, so the order cancels out and strips with only those reads draw. A FLAT_SHADE input without pass-through that is read through `v_interp_mov_f32` P10 and not P20 [returns the provoking vertex's value](../../../core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp), like P0. ps5-opengl reads every flat input that way, and its driver passes the GL 4.6 CTS on a PS5 (#1413). The lane itself was not measured on the console, so with the first provoking vertex such a shader throws. - [Dual-source blending](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) takes the MRT1 export as the second source of the MRT0 blend, as RADV and radeonsi program it; what the CB does with other export or target layouts is unknown, so they throw. - [Blending into reversed-order color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - 8_8_8_8 and 16_16_16_16 targets with `COMP_SWAP` 2 or 3 keep the exported alpha in their first channel and red or blue in their fourth, which Vulkan would blend with the color and the alpha equation respectively. The color block is taken to blend in shader export order and apply `COMP_SWAP` afterwards: the alpha equation and every source-alpha factor use the exported alpha, and the destination alpha is the first channel. This follows the `CB_TARGET_MASK` text of the AMD [SI](https://xorg.freedesktop.org/docs/AMD/old/SI_3D_registers.pdf) (p. 229) and [CIK](https://xorg.freedesktop.org/docs/AMD/old/CIK_3D_registers_v2.pdf) (p. 238) register references ("the order of components after blending and before COMP_SWAP is applied"). RADV builds `CB_BLENDn_CONTROL` without regard to the swap ([radv_pipeline_graphics.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/vulkan/radv_pipeline_graphics.c#L650-L709)) and renders R4G4B4A4, B4G4R4A4, R5G5B5A1 and B5G5R5A1 with `SWAP_STD_REV` and `SWAP_ALT_REV` ([vk_format.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/vulkan/util/vk_format.c#L43-L48), [ac_formats.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/common/ac_formats.c#L609-L624)). Mesa CI's Navi10 (gfx1010) CTS run ([job 112381553](https://gitlab.freedesktop.org/mesa/mesa/-/jobs/112381553), [112381554](https://gitlab.freedesktop.org/mesa/mesa/-/jobs/112381554)) passes all 400 `blend.format..states` cases of each of those formats, including separate alpha and destination-alpha factors. It is not measured on gfx1013 or on 8_8_8_8 and 16_16_16_16 targets. Factor pairs Vulkan applies per channel are used directly. Otherwise the pixel shader only copies values: it writes the exported alpha, each channel's own export or the constants 0 and 1 into a second color at location 0 index 1 (and 1 into a channel of the first color whose source factor is ONE, moving its export into the factor), and the blend forms every factor, `1 -` and product from those copies through `SRC1_COLOR`/`SRC1_ALPHA` factors. The blend unit therefore rounds them as it rounds a plain Vulkan blend of the same export; Vulkan does not require a second-source color to be converted like the first, but on an RTX 5080 and lavapipe every reversed-order case of the execution test is bit-identical to the same draw into a `SWAP_STD` target, which the test checks. Factor combinations that copies cannot express (for example `ONE_MINUS_SRC_ALPHA` in the alpha equation with `SRC_COLOR` in the color equation), sRGB, SNORM and integer targets, constant and second-source factors, destination-alpha and `SRC_ALPHA_SATURATE` factors in the color equation, different color and alpha operations, MIN or MAX with factors other than ONE, other color exports when the blend needs the second color, and devices without `dualSrcBlend` throw. Gran Turismo 7 (PPSA01317) blends `CB_BLEND0_CONTROL` 0x65010504 into such a target (#4782). - [DB_SHADER_CONTROL.CONSERVATIVE_Z_EXPORT](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp) (libSceAgcDriver) becomes `DepthLess`/`DepthGreater`. For a pixel shader that breaks the promise, the host driver decides whether its interpolated Z is depth-tested before shading; what the console does then was not measured. -- [DB_SHADER_CONTROL.STENCIL_TEST_VAL_EXPORT_ENABLE](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) becomes `FragStencilRefEXT` when available, taken from the second MRTZ channel of the 32_GR and 32_ABGR formats as Mesa's AMD drivers write it ([`emit_ps_mrtz_export`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/3e32b0e1f132231b7a972c1b0b5b06c7e16cf8b9/src/amd/common/nir/ac_nir_lower_ps_late.c#L194)); the console was not measured. The host value is the test value of both faces and what `REPLACE_TEST` writes, so a stencil operation that writes all ones or the operation value throws with it, as do a stencil clear draw, the UINT16_ABGR layout and `STENCIL_OP_VAL_EXPORT_ENABLE`. A device without the extension uses the bounded replay described above. On such a device a draw first drops the export when the reference cannot change its result (`StencilReferenceUnused`: no stencil test or attachment, or on both faces a compare that ignores it and no reachable `REPLACE` through a non-zero write mask), and its pixel shader is then compiled at its first draw, not when its ABI is resolved. A device with the extension keeps the export, so each such shader keeps one pixel variant. +- [DB_SHADER_CONTROL.STENCIL_TEST_VAL_EXPORT_ENABLE](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) becomes `FragStencilRefEXT` when available, taken from the second MRTZ channel of the 32_GR and 32_ABGR formats as Mesa's AMD drivers write it ([`emit_ps_mrtz_export`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/3e32b0e1f132231b7a972c1b0b5b06c7e16cf8b9/src/amd/common/nir/ac_nir_lower_ps_late.c#L194)); the console was not measured. The host value is the test value of both faces and what `REPLACE_TEST` writes, so a stencil operation that writes all ones or the operation value throws with it, as do a stencil clear draw and the UINT16_ABGR layout. A device without the extension uses the bounded replay described above. On such a device a draw first drops the export when the reference cannot change its result (`StencilReferenceUnused`: no stencil test or attachment, or on both faces a compare that ignores it and no reachable `REPLACE` through a non-zero write mask), and its pixel shader is then compiled at its first draw, not when its ABI is resolved. A device with the extension keeps the export, so each such shader keeps one pixel variant. +- [DB_SHADER_CONTROL.STENCIL_OP_VAL_EXPORT_ENABLE](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - the pixel shader supplies the stencil operation source value per pixel, in place of `DB_STENCILREFMASK.STENCILOPVAL`. Only the operations that take that value read it: `REPLACE_OP` and the clamped and wrapping add and subtract (codes 4, 5, 6, 8 and 9), and the logic operations, which throw already ([AMD_stencil_operation_extended](https://registry.khronos.org/OpenGL/extensions/AMD/AMD_stencil_operation_extended.txt), [AMD_shader_stencil_value_export](https://registry.khronos.org/OpenGL/extensions/AMD/AMD_shader_stencil_value_export.txt)). Vulkan has no per-pixel operation value. Without `STENCIL_TEST_VAL_EXPORT_ENABLE`, a `REPLACE_OP` reachable through a non-zero write mask (`StencilOperationValueReplaces`) becomes `REPLACE` with the exported value as `FragStencilRefEXT`, or the bounded replay without the extension; this requires a compare that ignores the test value and no `ONES` or `REPLACE_TEST` on either face, and early tests throw. The value is taken from bits 8-15 of the second MRTZ channel: the PPSA27836 pixel shader that uses it shifts it left by 8 before `exp mrtz` (`v_lshlrev_b32 v0, 8, v0`), which is the only evidence for the layout. The add and subtract operations, and `REPLACE_OP` with a test value export, throw. With the 32_GR or 32_ABGR Z format and no such operation, or without a stencil test, the export cannot change the result and is ignored. Other Z formats throw. The console was not measured. - [DB_SHADER_CONTROL.DEPTH_BEFORE_SHADER](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp) (libSceAgcDriver) becomes `EarlyFragmentTests`, and so does `Z_ORDER` `EARLY_Z_THEN_LATE_Z` or `EARLY_Z_THEN_RE_Z` without kill, Z export, mask export or stencil export; with alpha-to-coverage the tests stay late. The meaning of the bit and of `Z_ORDER` 3 follows Mesa (`src/amd/registers/gfx10.json`; RADV sets the bit for `early_fragment_tests` in `src/amd/vulkan/radv_shader.c`). What the console does with `DEPTH_BEFORE_SHADER` and a Z export, a mask export, a stencil export or alpha-to-coverage together was not measured. - [PA_SC_CONSERVATIVE_RASTERIZATION_CNTL](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - 0x6001 uses Vulkan overestimation on devices that add at most 1/256 pixel and keep degenerate triangles; the console's uncertainty region and degenerate handling are unmeasured. A value with `OVER_RAST_ENABLE` and `UNDER_RAST_ENABLE` clear is decoded as disabled even when `UNCERTAINTY_REGION_MODE` or the `OUTER_`/`INNER_UNCERTAINTY_EDGERULE_OVERRIDE` bits (19:16) are set, as PPSA01474 writes 0xe6000; that these fields have no effect without over- or underestimation is assumed from their names, not measured. With `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES` = 0 the `PERSP_CENTROID` and `LINEAR_CENTROID` inputs get the pixel-centre barycentrics, as without conservative rasterization ([shader debt](shader.md)): the one rasterized sample sits at the pixel centre, so every centroid the rasterizer could pick for an overestimated pixel is that point; that it does not move the centroid towards the primitive when the sample is uncovered is inferred, not measured. With more rasterized samples, and when a single-sample draw is raised to a multisampled color target, centroid inputs throw. - [Descriptor pool chain](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - each pool holds two storage buffer descriptors more than a set can take, as a workaround: RADV (Mesa 26.0 and later) lets a `FREE_DESCRIPTOR_SET` pool place a set up to 32 bytes past the pool's buffer ([`util_vma_heap_init`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/vulkan/radv_descriptor_pool.c) is given `bo_size + RADV_POOL_HEAP_OFFSET` as a size), and with a page-multiple pool size those bytes are an unmapped page - [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 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 where they fill a whole 8 MiB aligned window; ranges of a partly filled window, and 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. +- [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 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. - [Multisampled color fills](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - a multisampled target's samples are on the host, so a fill of its color memory is applied at its next use: a uniform fill over the whole of it clears the host image to that value, and a pending CMASK fast clear takes precedence over the fill and over a fill that would throw. Its color memory is a single mip and slice of addrlib's block dimensions for its sample count (ComputeThinBlockDimension). A fill over part of it, a fill reaching the start of a target whose footprint is not computed (linear), and a fill value with no exact clear value in the format (sRGB channels other than 0 and 1) throw at the next use. A fill before the target's first use is not seen (the host image starts at 0), nor are copies or shader writes into its color memory. A uniform compute fill over exactly the whole color memory with an exact clear value is not stored into guest memory, since only the host image holds the samples. A target is retired only when its whole guarded range is unmapped or its allocation removed, so if a title reuses that memory for other data while it stays mapped, a read the guard does not check (a buffer read through a V# or a pointer) sees the bytes from before the fill. - [Multisampled texture no draw rendered to](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - sampling a multisampled texture whose surface no draw rendered to creates the host target from the T# and clears it to the color a single 32-bit value repeated over the whole footprint encodes, padding up to whole tile blocks included. Inferred, not measured on the console: that the samples of such a surface are the guest color memory, as a title that clears its canvas on the CPU (ps5-opengl `ps5_clear_msaa4_color`) relies on. Memory that is not one repeated value, tile modes other than Standard4KB, Standard64KB and RenderTarget, unmapped memory and values with no exact clear color throw. CPU writes to the surface after its host target exists are not seen by the host. - [Multisampled targets in CB passes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a CB eliminate or DCC pass over a multisampled target throws, since its texels are on the host and only a CB resolve and an FMASK decompress (`CB_COLOR_CONTROL` mode 5) are modeled. The FMASK decompress only applies the pending CMASK fast clear to the host image; it requires a CMASK address, and the target's DCC and FMASK contents are not interpreted. DCC over a multisampled target is accepted only in that pass and throws everywhere else. A CB resolve into a fast-clear destination throws: the resolve writes the destination's texels but not its CMASK, which would still read as cleared. -- [Multisampled target guest access](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - the memory a multisampled color target throws on (CPU read, index buffer read, fixed-function vertex buffer read, DMA copy, texture or storage image read) uses fragment-aware 64 KiB render-target blocks. [AMD Addrlib's GFX10 thin-block rule](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/core/addrlib2.cpp#L1680-L1712) gives the block width and height from element bytes and fragment count; its [surface-size calculation](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp#L4000-L4005) rounds both extents to those blocks and multiplies by element bytes and fragments. Thus a 1920x1080 4x RGBA8 surface occupies 0x1fe0000 bytes, leaving the next allocation at +0x2000000 outside the guard. The 2x and 8x block orientations differ from treating the fragments as a larger element. Console address-layout equivalence has not been measured. Other tile modes retain the conservative `max(CB bytes * samples, width * height * element bytes * samples)` bound, which may still reject a distinct allocation. Live targets remain cached per device, surface address, format, extent and sample count. Retired cache owners are removed on the next target, sampled or storage lookup; outstanding recorded work and sampled views retain their images. +- [Multisampled target guest access](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - the memory a multisampled color target throws on (CPU read, index buffer read, fixed-function vertex buffer read, DMA copy, texture or storage image read) uses fragment-aware 64 KiB render-target blocks. [AMD Addrlib's GFX10 thin-block rule](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/core/addrlib2.cpp#L1680-L1712) gives the block width and height from element bytes and fragment count; its [surface-size calculation](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp#L4000-L4005) rounds both extents to those blocks and multiplies by element bytes and fragments. Thus a 1920x1080 4x RGBA8 surface occupies 0x1fe0000 bytes, leaving the next allocation at +0x2000000 outside the guard. The 2x and 8x block orientations differ from treating the fragments as a larger element. Console address-layout equivalence has not been measured. Other tile modes retain the conservative `max(CB bytes * samples, width * height * element bytes * samples)` bound, which may still reject a distinct allocation. Live targets remain cached per device, surface address, format, extent and sample count. Retired cache owners are removed on the next target, sampled or storage lookup; outstanding recorded work and sampled views retain their images. Ordinary sampled and storage bindings check the color-target guard before resource reuse, including precollected sampled lookups. Reused image tables do not invalidate on multisample color-target registration yet; their per-element fault handling and native multisample sampling require a separate reuse proof. - [CB resolve sample count](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a draw with CB_COLOR_CONTROL in resolve mode is a CB resolve when either PA_SC_AA_CONFIG or CB_COLOR0_ATTRIB.NUM_SAMPLES is multisampled, and takes its sample count from CB0. Inferred, not from documentation: PPSA36082 resolves its 4x targets with no sample count in PA_SC_AA_CONFIG, and radeonsi sets PA_SC_AA_CONFIG to 0 for a multisampled framebuffer when rasterizer multisampling and line smoothing are off. PA_SC_AA_CONFIG is only compared with CB0 while PA_SC_MODE_CNTL_0.MSAA_ENABLE is set: PPSA08270 resolves a 4x 512x512 target with MSAA_ENABLE off and PA_SC_AA_CONFIG still at 2x (0x00108001) from earlier draws. -- [Custom MSAA sample locations](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) support a uniform 1x1 pattern for 2, 4 or 8 samples through VK_EXT_sample_locations. Patterns varying across the guest's 2x2 pixel grid use a 2x2 sample location grid, anchored at the framebuffer origin like the guest's while the window offset is zero, on devices whose `maxSampleLocationGridSize` for the sample count is a multiple of 2x2. On other devices every pixel takes the pattern's pixel (0,0) locations (`PA_SC_AA_SAMPLE_LOCS_PIXEL_X0Y0_*`, its first registers and the corner the grid is anchored at), and the first such pipeline of each sample count logs `[gpu] no 2x2 sample location grid`. The RTX 5070 reports 2x4 for 2 samples, 2x2 for 4 and 1x2 for 8; an AMD Radeon integrated GPU reports 2x2 for each; an RTX 3070 Laptop GPU (driver 617.42) reports 1x1 for every count. With that stand-in the top-left pixel of every 2x2 quad matches the console and the other three use its positions. A pattern that only reorders one set of positions between pixels, as a 2x checkerboard pattern puts its samples at x = +4/16 and -4/16 and swaps them on odd rows, keeps every position and the number of samples a primitive covers in each pixel, so a resolve that averages a pixel's samples, or takes their minimum or maximum, sees the console's values. Sample i of an odd row then holds what the console stores in sample 1 - i: a pass that reads the samples by index, as checkerboard reconstruction does, exchanges the two half-pixels of odd rows, which can show as combing along vertical edges, and per-sample shading, sample-mask exports and coverage inputs see the swapped indices too. A pattern whose pixels hold different positions draws pixel (0,0)'s positions in each pixel whose own differ. A device's pipelines and render pass begins take the same stand-in for a given pattern, so a sample-location-compatible depth image is not drawn with one set of locations and transitioned or tested with another. A multisampled rasterizer drawing into one-sample color targets, and a one-sample depth target if any, with MSAA_ENABLE set, no alpha-to-coverage and no coverage pixel input is drawn with one sample at sample 0's location in each pixel of the grid, as the targets keep only sample 0's coverage. Without PS_ITER_SAMPLE such a draw also refuses centroid inputs, and keeps PERSP_SAMPLE, LINEAR_SAMPLE and ANCILLARY: a pixel shaded once is taken to get sample 0's barycentrics and sample index 0. With PS_ITER_SAMPLE only sample 0's invocation reaches the targets, so its sample and centroid barycentrics are taken to be sample 0's and the draw is shaded once. Both are inferred, not measured. The pixel shader runs once per pixel with sample number 0, so ANCILLARY reads sample 0 and POS_X_FLOAT and POS_Y_FLOAT keep the position `SPI_BARYC_CNTL` names. PERSP_CENTER and LINEAR_CENTER inputs on this path are interpolated at sample 0's location on the host, where the console uses the pixel center; one-sample depth images are created with `SAMPLE_LOCATIONS_COMPATIBLE_DEPTH` when the device supports it, as multisampled ones already were; this is how VK_AMD_mixed_attachment_samples defines the case, and it is not measured on PS5 hardware. Other mixed sample counts throw. Devices without the required sample count, four subpixel bits or the full guest coordinate range refuse custom patterns. Depth images remain in GENERAL between draws, and render passes cannot continue across pattern changes. Guest offsets retain the existing decoder's signed four-bit, sixteenth-pixel interpretation; PS5 coverage and interpolation equivalence have not been measured on hardware. +- [Custom MSAA sample locations](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) support a uniform 1x1 pattern for 2, 4 or 8 samples through VK_EXT_sample_locations. Patterns varying across the guest's 2x2 pixel grid use a 2x2 sample location grid, anchored at the framebuffer origin like the guest's while the window offset is zero, on devices whose `maxSampleLocationGridSize` for the sample count is a multiple of 2x2. On other devices every pixel takes the pattern's pixel (0,0) locations (`PA_SC_AA_SAMPLE_LOCS_PIXEL_X0Y0_*`, its first registers and the corner the grid is anchored at), and the first such pipeline of each sample count logs `[gpu] no 2x2 sample location grid`. The RTX 5070 reports 2x4 for 2 samples, 2x2 for 4 and 1x2 for 8; an AMD Radeon integrated GPU reports 2x2 for each count, per count and for every count; an RTX 3070 Laptop GPU (driver 617.42) reports `maxSampleLocationGridSize` 1x1 for every count (`VkPhysicalDeviceSampleLocationsPropertiesEXT`), but per count (`VkMultisamplePropertiesEXT`, which the device setup reads and logs) a 2x4 grid at 2 samples, 2x2 at 4 and 1x2 at 8. So the 3070's 2x patterns, the 2x checkerboard among them, take the native grid, and the stand-in plus the exact path serve its 8-sample patterns and devices without a grid. A pattern that only reorders one set of positions between pixels, as a 2x checkerboard pattern puts its samples at x = +4/16 and -4/16 and swaps them on odd rows, is drawn exactly through that stand-in: before every render pass that draws with it each pixel's samples are put in the stand-in's order and after it back in the guest's, through a storage view of a multisampled color target and through a copy and a per-sample pass for depth (writing stencil through VK_EXT_shader_stencil_export, or one pass per bit without it), while the pixel shader reads SampleID, applies the sample mask and exports coverage in the guest's order, and the second log line names the reordering. A uniform partial sample mask that the order moves is applied in the pixel shader. With early Z, a depth or stencil test and memory writes in the pixel program, the draw throws, where #6451's stand-in alone ran it with odd rows' sample indices swapped. A fix would choose the stand-in after compilation. That costs two passes over each multisampled attachment per render pass, and on those devices every multisampled color target is created with a mutable format and storage usage for its view, which a driver may answer by not compressing it. It needs shaderStorageImageMultisample with a storage view of the color format at the sample count, sampleRateShading, and a depth format a FragDepth write gives back exactly (16-bit, or 32-bit float; not 24-bit depth, the stand-in for 16-bit depth with stencil where the device has neither); without them, and for patterns whose pixels hold different positions, the stand-in alone is used, and the log line says why. A device without shaderStorageImageMultisample takes the stand-in alone for every pass, depth-only passes included, so a depth target that a depth-only pass and a color pass both draw keeps one order. On a device that has it, a pass is still refused for a color format without a storage view at its sample count, while a depth-only pass on the same depth target is reordered, so that target's samples change order between those passes; a fix would decide the order per target. Then the top-left pixel of every 2x2 quad matches the console and the other three use its positions. A same-set pattern keeps every position and the number of samples a primitive covers in each pixel, so a resolve that averages a pixel's samples, or takes their minimum or maximum, sees the console's values, but sample i of an odd row holds what the console stores in sample 1 - i: a pass that reads the samples by index, as checkerboard reconstruction does, exchanges the two half-pixels of odd rows, which can show as combing along vertical edges, and per-sample shading, sample-mask exports and coverage inputs see the swapped indices too. A pattern whose pixels hold different positions draws pixel (0,0)'s positions in each pixel whose own differ. A device's pipelines and render pass begins take the same stand-in for a given pattern, so a sample-location-compatible depth image is not drawn with one set of locations and transitioned or tested with another. A multisampled rasterizer drawing into one-sample color targets, and a one-sample depth target if any, with MSAA_ENABLE set, no alpha-to-coverage and no coverage pixel input is drawn with one sample at sample 0's location in each pixel of the grid, as the targets keep only sample 0's coverage. Without PS_ITER_SAMPLE such a draw also refuses centroid inputs, and keeps PERSP_SAMPLE, LINEAR_SAMPLE and ANCILLARY: a pixel shaded once is taken to get sample 0's barycentrics and sample index 0. With PS_ITER_SAMPLE only sample 0's invocation reaches the targets, so its sample and centroid barycentrics are taken to be sample 0's and the draw is shaded once. Both are inferred, not measured. The pixel shader runs once per pixel with sample number 0, so ANCILLARY reads sample 0 and POS_X_FLOAT and POS_Y_FLOAT keep the position `SPI_BARYC_CNTL` names. PERSP_CENTER and LINEAR_CENTER inputs on this path are interpolated at sample 0's location on the host, where the console uses the pixel center; one-sample depth images are created with `SAMPLE_LOCATIONS_COMPATIBLE_DEPTH` when the device supports it, as multisampled ones already were; this is how VK_AMD_mixed_attachment_samples defines the case, and it is not measured on PS5 hardware. Other mixed sample counts throw. Devices without the required sample count, four subpixel bits or the full guest coordinate range refuse custom patterns. Depth images remain in GENERAL between draws, and render passes cannot continue across pattern changes. A pass that draws depth at custom sample locations ends with a transition to TRANSFER_SRC_OPTIMAL and back that names them. Guest offsets retain the existing decoder's signed four-bit, sixteenth-pixel interpretation; PS5 coverage and interpolation equivalence have not been measured on hardware. - [Single-sample scan into a multisampled depth target](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a draw with `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES` = 0 and `PA_SC_MODE_CNTL_0.MSAA_ENABLE` clear into a multisampled depth target rasterizes the depth target's sample count with every sample at the pixel center, as a single-sample scan into a multisampled color target already does. radeonsi programs this `MSAA_ENABLE` and `PA_SC_AA_CONFIG` with the depth buffer's own sample count when GL multisampling is disabled on a multisampled framebuffer ([L1066-L1068](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/gallium/drivers/radeonsi/si_state.c#L1066-L1068), [L3485-L3512](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/gallium/drivers/radeonsi/si_state.c#L3485-L3512)), although its comment lists Z samples as at most the coverage samples ([L3439](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/gallium/drivers/radeonsi/si_state.c#L3439)) and it sets `DB_EQAA.MAX_ANCHOR_SAMPLES` to the Z sample count, where PPSA18216 sets `DB_EQAA` to 0. GL then lets every sample take the single-sample depth ([L185-L190](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L185-L190)) and the fragment be treated as in multisampling with full coverage ([L422-L425](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L422-L425)), which is what this draws: each depth and stencil sample is tested and written with the depth at the pixel center. GL also lets an implementation test only a centermost sample and set every sample from it ([L425-L434](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L425-L434)), which differs when a pixel's stored samples differ; which one the console does, whether it instead evaluates the depth plane at each sample position, and how its occlusion queries count such a draw, is not measured on hardware. Points, lines, alpha-to-coverage, `DB_EQAA` Z interpolation or overrasterization, a pixel program reading the sample index, a sample-mask export and a single-sample color target next to the multisampled depth target throw - [DCC display buffers](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) are presented only when all keys of the console's DCC extent agree on uncompressed or a fast-clear code, padding keys included; other keys, and register-clear keys over a pending image, throw. -- [Storage image write-back](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) stores the GPU results over a CPU-stamped range whose guest bytes equal the copy taken when the image's layers got their generation (images up to 1 MiB whose guest bytes were settled at that point, with no unit shadow, pending image results or recorded GPU store over them; other images keep the previous rule: the CPU's bytes win). A CPU store that rewrites the baseline value after the GPU changed it is not told apart from no store, and the GPU result wins there. The 1 MiB limit is a choice, not a console value. +- [Storage image write-back](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) stores the GPU results over a CPU-stamped range whose guest bytes equal the copy taken when the image's layers got their generation (images up to 1 MiB whose guest bytes were settled at that point, with no unit shadow, pending image results or recorded GPU store over them; other images keep the previous rule: the CPU's bytes win). A CPU store that rewrites the baseline value after the GPU changed it is not told apart from no store, and the GPU result wins there. The 1 MiB limit is a choice, not a console value. A range a queued draw commit still has to read is left pending whatever the stamps say ([`DrawPipeline::HeldReads`](../../../core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp)), so a title that recycles one allocation between an index buffer and a storage image sees the draw's own indices; the trade is a title that reads a surface as index data and means the image gets the pre-image bytes instead, since the reservation does not compare the command-stream positions of the draw and of the image's write. `APS5_NO_QUEUED_DRAW_READ_ORDER=1` restores the old behaviour, and the `[storage]` line counts the listings the hold turned away (`held for a queued draw read`) and the `[draw] pipelined` line the stores it held. The hold covers what the commit reads of guest memory: the index range, the vertex input ranges as their descriptor extents ([`QueuedDrawReadRanges`](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WrittenBuffers.cpp), since the bytes a draw fetches depend on the highest index, which only the commit knows) and the source of a queued DMA copy. Every other commit-time read goes through the flush hook, so it sees the bytes the hold protects. Two commit-time reads are outside the hold: the texels a register or cmask clear materialization reads from the draw's own colour target (through the hook, so ordered, but the hold does not cover a render target), and a CPU read whose store the hold turned away, which sees the pre-image bytes. Nothing retries a refused store: the image reaches guest memory with the next flush of that range (the queue worker's idle store, a `WAIT_REG_MEM` poll over it, a later read or write of it). A kept 64 KiB unit that lies outside every (slice, level) window stores nothing: the tracking grid is flat over the guest range while 3D slices taper in depth with the mip level (`HasLayer`), so the higher depth slices' higher-level blocks belong to no window. The console's tracking of such units is not measured. - [16-bit float display buffers](../../../core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp) (libSceAgcDriver, libSceVideoOut) - pixel formats `0xC001000622000000` and `0xC001000600000000` are taken as R16G16B16A16 and B16G16R16A16 float, eight bytes per texel, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/59183dbfa81ed7efdff6069177fed50de0552725/src/graphics/host_gpu/renderer/image/imageInfo.h) maps them and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/VideoOut/VideoOutTypes.cs) names them (`Rgba16Float`, `Bgra16Float`); PPSA11386 registers 3840x2160 tiled buffers of the first. The tiled layout is assumed to be the 8-byte SW_64KB_R_X equation of `TextureSwizzleEquations.hpp` applied to full coordinates, as addrlib applies it, by analogy with the 32-bit scanout; `ColorTargetLayout` applies the equations to in-block coordinates, which for 8-byte texels differs in every odd 64-row block row (address bit 11 takes y bit 6) and for 16-byte texels also differs, so a float display buffer written through `WriteColorTarget` and presented from guest memory would show those rows misplaced. The values are assumed to be linear light and are encoded with the sRGB curve for the 8-bit output. Bits 32-39 of these formats are `0x06`, as in the 10-bit formats SharpProspero lists without sRGB transfer, while its sRGB formats have `0x00`, its 10-bit PQ formats `0x04` (with `0x07` in bits 40-47) and its float BT.2100 PQ formats `0x07`: they show only that these formats are not the sRGB ones. NaN and negative values give 0, values of 1 and above give 255, alpha is linear. Neither the layout nor the transfer is verified against console or title frames, and HDR output is not modelled. DCC metadata on a float buffer throws. - [BT.2100 PQ display buffers](../../../core/libs/prx/libSceAgcDriver/Execution/include/PqToSdr.hpp) (libSceAgcDriver, libSceVideoOut) - pixel formats `0x8100070422000000` and `0x8100070400000000` are taken as 10:10:10:2 texels with BT.2020 primaries and the SMPTE ST 2084 transfer, red or blue in the low bits, as [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/VideoOut/VideoOutTypes.cs) names them (`Rgb10A2Bt2100Pq`, `Bgr10A2Bt2100Pq`) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/59183dbfa81ed7efdff6069177fed50de0552725/src/graphics/host_gpu/renderer/image/imageInfo.h) maps the first; PPSA01317 registers display buffers of the first when its output reports HDR. With HDR output they are presented unchanged through an HDR10 swapchain (`VK_COLOR_SPACE_HDR10_ST2084_EXT`, 10-bit, with `VK_EXT_hdr_metadata`); otherwise they are converted to 8-bit sRGB: ST 2084 EOTF, BT.2020 to BT.709 primaries with negative components clipped, a luminance tone map and the sRGB encoding. The tone map is ours: min(203 nits, white level) is SDR white (the BT.2408 HDR reference white), luminance passes unchanged up to 0.75 of it and an extended Reinhard shoulder above reaches 1.0 at the white level, the peak sceSystemServiceGetHdrToneMapLuminance last reported to the title (or the current one when it never asked), since titles read it once. How a console shows such a buffer on an SDR display is unknown. Alpha is opaque. +- [HDR output override](../../../core/libs/prx/libSceAgcDriver/Execution/src/HostHdr.cpp) - setting `APS5_FORCE_SDR` suppresses HDR availability reported to titles while retaining the detected display and surface capabilities. It selects the existing SDR presentation path; it does not add support for floating-point BT.2100 PQ buffers (`0xC001070722000000` and `0xC001070700000000`), whose presentation remains refused. The override does not change the host display settings. - [DCC key stores](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp) cover the console's DCC extent only for pipe-aligned DCC of single-level, single-layer 4-byte SW_64KB_R_X surfaces; other surfaces, and every texture and color target key read, cover one byte per 256 bytes. - [DCC key fills over other images](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) - a uniform fill at a live image's `dccAddress`, at least one key per 256 bytes long, is a key fill of that image even when other live images overlap it, but only while it is no longer than the image's key extent (`DccKeyCount`, the pipe-aligned count); a longer one is classified by its overlaps as before. The bound is inferred, not measured on a title: a longer fill also writes the bytes past the keys, which the overlapping image may own. Whether the title's bloom-target key fills stay within the extent was not checked (`APS5_TRACE_DCC_KEYS` prints the length of the fills classified as keys, not of the refused ones). - [Comp-to-single DCC keys](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp) (libSceAgcDriver) - key 0x10 (`GFX9_DCC_CLEAR_SINGLE` in Mesa `ac_descriptors.h`) uniform over a surface makes every texel the element at the start of its 256-byte block, the top-left texel of the block where radv's `build_clear_dcc_comp_to_single_shader` stores the clear color. Sampled textures and storage images read that expansion (a storage image's first write stores the keys uncompressed, as under a clear code), and a CB DCC decompress pass stores it over the texels. A fast-clear eliminate over such keys throws, as a metadata pass over mixed keys does; color targets that render over such keys use their stored texels, and textures read mixed keys, and comp-to-single keys of a format whose element does not divide 256 bytes, as stored. "Uniform" is judged over the key bytes the driver reads, one per 256 surface bytes (see DCC key stores above), so a pipe-aligned surface whose keys differ past that range is expanded as if all were comp-to-single, as for the other clear codes. Not verified on hardware @@ -158,16 +174,16 @@ - [COPY_DATA from the GPU clock](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 9) writes the host steady clock in 10 ns ticks when the packet runs, the same counter as the `RELEASE_MEM` timestamp, not the console's GPU clock. The `COPY_DATA` cache policy fields are accepted and have no effect. - [COPY_DATA from a register](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 0) copies the value the queue holds for a shader, context or user config register (`COUNT_SEL` two consecutive ones) and throws for a register with no value. A dispatch records `COMPUTE_DISPATCH_INITIATOR` and, for `DISPATCH_DIRECT`, `COMPUTE_DIM_X/Y/Z` in the shader registers; `DISPATCH_INDIRECT` clears the three counts. Neither the register values the CP writes nor the copy's ordering against in-flight dispatches have been measured on hardware. - [EVENT_WRITE stream-out statistics events](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, opcode 0x46) - `SAMPLE_STREAMOUTSTATS1`-`3` and `SAMPLE_STREAMOUTSTATS` (event types 0x01-0x03 and 0x20 in [navi10_enum.h](https://github.com/torvalds/linux/blob/0fcb05b72344f40806cbe810de8872f1698bf053/drivers/gpu/drm/amd/include/navi10_enum.h#L14155-L14188)) are accepted on the graphics queue as a 2-dword packet with event index 0 and do nothing. [nvd.h](https://github.com/torvalds/linux/blob/0fcb05b72344f40806cbe810de8872f1698bf053/drivers/gpu/drm/amd/amdgpu/nvd.h#L326-L347) documents these events only with the sampling index (3, or `EVENT_INDEX__SAMPLE_STREAMOUTSTATS*` 8-11 on gfx10); the index-0 packet has no destination dwords, and what the CP does with it is neither documented nor measured on hardware. AnyPS5 does not emulate the VGT stream-out counters, so there is nothing to sample. The sampling indices throw that sampling to memory is not implemented, and other indices, other sizes and the compute queue throw. [sceAgcDcbEventWrite](../../../core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp) ignores the address for every event type except 0x38 and 0x39, so a call with one of these event types and an address builds the index-0 packet: nothing is written and nothing throws. Whether the console's builder emits a sampling packet for an address is not known. -- [INDEX_ATTRIBUTES_INDIRECT](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, opcode 0x91) loads INDEX_BASE, INDEX_BUFFER_SIZE (in indices) and INDEX_TYPE from the 16-byte record at `attribute_base + attribute_index * 16`, read when the packet runs. The record layout (address low, address high, size, type) is PAL's [`IndexAttribIndirect`](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9Chip.h#L60-L67) and the packet is the one [`BuildIndexAttributesIndirect`](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9CmdUtil.cpp#L2586-L2613) writes; neither is measured on a PS5 or seen in a title. `indirect_mode` 1 (`addr_offset`, which only PAL's ExecuteIndirect path emits) throws, and so does a record whose index type is above 3, as INDEX_TYPE does. +- [INDEX_ATTRIBUTES_INDIRECT](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, opcode 0x91) loads INDEX_BASE, INDEX_BUFFER_SIZE (in indices) and INDEX_TYPE from the 16-byte record at `attribute_base + attribute_index * 16`, read when the packet runs. The record layout (address low, address high, size, type) is PAL's [`IndexAttribIndirect`](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9Chip.h#L60-L67) and the packet is the one [`BuildIndexAttributesIndirect`](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9CmdUtil.cpp#L2586-L2613) writes. The console's `sceAgcDcbSetIndexIndirectArgs` writes this packet, with `indirect_mode` 0, the attribute base in the address dwords and the attribute index in bits 0-15 ([measured](https://github.com/mattias800/prosper/blob/3ea5cb9a846b6dcd00f5073360462bbda4f72288/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L172-L174) by the prosper console oracle); the record layout is not measured on a PS5, and neither is seen in a title. `indirect_mode` 1 (`addr_offset`, which only PAL's ExecuteIndirect path emits) throws, and so does a record whose index type is above 3, as INDEX_TYPE does. - [16-bit RGBA color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) support the standard and the alternate (`COMP_SWAP` 1) component order. Alternate swaps red and blue in shader exports, write masks and constant-color blend factors, following `ac_translate_colorswap` in [Mesa 26.2.2 ac_formats.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/common/ac_formats.c). Blending into the reverse orders (`COMP_SWAP` 2 and 3) is covered by the reversed-order blending entry, and simultaneous targets requiring conflicting blend constants throw. Console equivalence has not been measured on hardware. - [UINT16_ABGR color exports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - export format 7 is accepted only into an unsigned integer target, and each 16-bit value reaches the target unchanged; a value beyond a narrower target's range (`R8_UINT`) is undefined in Vulkan, and whether the console clamps or truncates it is not confirmed. SINT16_ABGR (8) throws -- [Layered color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - only 3D color targets with 64 KiB render-target or 4 KiB standard tiling render several slices: a `CB_COLOR_VIEW` slice range on an array target, a layered draw with a depth target, and color targets of different slice counts throw. The vertex layer export (`PA_CL_VS_OUT_CNTL` USE_VTX_RENDER_TARGET_INDX) reaches the shader only for a layered draw and is otherwise ignored, as before; a layer beyond the view's last slice is undefined in Vulkan, while the console's handling (clamping to `SLICE_MAX`) is not modeled, and a view whose last slice lies past the surface is clamped to its last slice. DCC keys of a 3D color target are taken as one key per 256 bytes over the whole surface, every slice included, and only uniform keys (all uncompressed, or one fast-clear code) are interpreted, as for 2D targets; mixed keys leave the stored texels as they are. The console's DCC key layout for 3D surfaces is not checked (LEGO Horizon Adventures' (PPSA14632) lighting volumes are DCC) +- [Layered color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - 3D color targets with 64 KiB render-target or 4 KiB standard tiling render several slices, and so does a `CB_COLOR_VIEW` slice range on a 2D array target through the array's resident image, or on a surface of one slice as an array of the view's last slice + 1 slices, one slice pitch apart: a layered draw into an array without its resident image, a layered draw with a depth target, and color targets of different slice counts throw. The vertex layer export (`PA_CL_VS_OUT_CNTL` USE_VTX_RENDER_TARGET_INDX) reaches the shader only for a layered draw and is otherwise ignored, as before; a layer beyond the view's last slice is undefined in Vulkan, while the console's handling (clamping to `SLICE_MAX`) is not modeled, and a view whose last slice lies past the surface is clamped to its last slice. DCC keys of a 3D color target are taken as one key per 256 bytes over the whole surface, every slice included, and only uniform keys (all uncompressed, or one fast-clear code) are interpreted, as for 2D targets; mixed keys leave the stored texels as they are. The console's DCC key layout for 3D surfaces is not checked (LEGO Horizon Adventures' (PPSA14632) lighting volumes are DCC) - [VGT_TF_PARAM](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - DISTRIBUTION_MODE is taken as scheduling only; the host tessellator decides the order of a patch's triangles. - [Local programs](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) that the hull header leaves at address 0 are prepared at their first draw, one artifact per tessellation configuration, instead of at registration. - [Packed 16-bit textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp) (libSceAgcDriver) - `5_6_5`, `1_5_5_5` and `4_4_4_4` are viewed as R5G6B5, A1R5G5B5 and B4G4R4A4 (the 4-bit format every Vulkan device can sample; R4G4B4A4 is optional and lavapipe lacks it) with a swizzle that reads X from the low bits, and a DST_SEL of the missing W of `5_6_5` reads X. Measured on RDNA2 (gfx1035), not on the console: through `image_load` and `image_sample_lz`, X, Y, Z (and W of `1_5_5_5` and `4_4_4_4`) come from the low, middle and high fields, and W of `5_6_5` returns X (the low 5 bits), not 1. - [Min/max S# reduction](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) - `FILTER_MODE` 1 and 2 use Vulkan's min and max reduction (the same `SQ_IMG_FILTER_MODE` values RADV programs). Measured on RDNA2 (gfx1035), not on the console: only texels of non-zero weight are reduced, and the weight is taken after rounding to the 8-bit subtexel grid (a sample within 1/512 texel of a centre reduces that texel alone); a driver with other `subTexelPrecisionBits` reduces a different set there. Gathers ignore the mode. - [Min/max S#](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver, shader recompiler) - throws with a linear mip filter, depth or color-texture compare, or bilinear or anisotropic filtering of an image the recompiler point-samples (sint, converted, depth-bits). With anisotropy the host sampler reduces over its own footprint; [RADV](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/amd/vulkan/radv_sampler.c#L185-L232) programs the same S# for such a Vulkan sampler. Measured on RDNA2 (gfx1030 in [#5156](https://github.com/boykopovar/AnyPS5/pull/5156#issuecomment-6155265616), gfx1035 in #5821), not on the console: without derivatives (`image_sample_lz`), min or max with anisotropic bilinear returns the bilinear minimum or maximum and with anisotropic point the point result, at every ratio field 0 to 4; with derivatives (`image_sample_d`, a 4:1 footprint), anisotropic bilinear at ratio field 2 or 4 reduces over the texels of all its anisotropic taps (the minimum or maximum along the major axis), not one bilinear footprint. A host that is not AMD reduces over its own anisotropic footprint, and Vulkan leaves nearest filtering with anisotropy to the host driver. -- [FORCE_DEGAMMA](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp) (libSceAgcDriver) - an S# that forces sRGB decoding adds nothing to an sRGB texture, as in shadPS4 (PS4). Measured on RDNA2 (gfx1035), not on the console: `8`, `8_8` and `8_8_8_8` sRGB textures sample bit for bit the same with and without the bit (point and bilinear; BC formats not measured). Other textures throw; on the same hardware their UNORM `8`, `8_8` and `8_8_8_8` forms sample bit for bit as the sRGB format (decoded before filtering). +- [FORCE_DEGAMMA](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp) (libSceAgcDriver) - not measured on the console. A sampler that forces sRGB decoding throws for an SNORM `8`, `8_8` or `8_8_8_8` texture, which RDNA2 (gfx1031) reads bit for bit as the sRGB format of the same bytes, and for a format not measured on RDNA2. The rest follows gfx1035 and gfx1031 (point and bilinear): sRGB textures sample as without the bit, as in shadPS4 (PS4); UNORM `8`, `8_8`, `8_8_8_8`, BC1, BC2, BC3 and BC7 sample through their sRGB view (decoded before filtering); `16`, `16_16` and `16_16_16_16` UNORM, SNORM and FLOAT, `32`, `32_32` and `32_32_32_32` FLOAT, `10_11_11`, `2_10_10_10`, `5_9_9_9`, `5_6_5`, `1_5_5_5`, `4_4_4_4`, BC4, BC5 and BC6H sample as without the bit, as gfx1031 does. - [Tessellation factor ring](../../../core/libs/prx/libSceAgcDriver/State/src/Configuration.cpp) (libSceAgcDriver) - `sceAgcDriverSetTFRing` records the ring and `sceAgcDriverGetTFRing` reports it. Before a title sets one, it reports the console's default, 0xFF0000000 and 128 KiB, as measured in [PS5_Vulkan's hardware findings](https://github.com/mihawk-99/PS5_Vulkan/blob/fde9e3798c2ec1d8fccc90d5067cd6060490607f/docs/HARDWARE_FINDINGS.md). Nothing is written to the recorded ring: tessellated draws run as Vulkan tessellation stages, which keep their factors on the host, so a title that reads tessellation factors from the ring finds none. A null base or a zero size throws; [ps5-opengl's AGC runtime](https://github.com/blackbearreloaded/ps5-opengl/blob/67c873f985eaefe0a72f713529e5632eaa01139c/src/platform/ps5_agc_native_runtime.c) calls `sceAgcDriverSetTFRing(0, 0)` in its cleanup, and what that does on the console is unknown. - [VertexBufferReadSize](../../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) and the [vertex staging copy](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - a vertex fetch whose index is at or past the descriptor's `num_records` no longer throws for `OOB_SELECT` 0 and 1. The copy still spans the whole fetch, but the bytes from `num_records * stride` onward read as zeros, so the out-of-range vertex is `(0, 0, 0, 0)`. Measured with `buffer_load_format_xyzw` (`idxen`) through the [hardware oracle](../HW_ORACLE.md) on RDNA2 gfx1036 for PPSA01325's word 3 `0x0004dfac` (`OOB_SELECT` 0) in #2241: indices 3, 4 and 5 over 3 records each return `00000000 00000000 00000000 00000000`, W included, unlike Vulkan `robustBufferAccess`, which returns `(0, 0, 0, 1)`. The zeros come from the record check, not from memory: stored values placed past the records are ignored, and a `num_records` 6 control returns them. `RESOURCE_LEVEL` 1, `OOB_SELECT` 1 and FORMAT 74 `32_32_32_FLOAT` give the same zeros, in wave32 and wave64; gfx1013 (the console) is not measured. `OOB_SELECT` 2 returns the stored values past the records and `OOB_SELECT` 3 compares `num_records` against the byte offset, both measured on the same setup in #2241; the two keep throwing past `num_records` here, since each needs its own rule. Zero-stride descriptors (`stride` 0) keep their separate byte-count check and are not covered here. Each out-of-range fetch is copied alone and outside the draw-snapshot cache, which is neither read nor written for it: a reused snapshot would carry another draw's bytes into the zero region, and a stored zero-padded copy would hand zeros to a later in-range draw at the same address. The valid prefix is still flushed for GPU writes before it is read. - [Draw snapshot buffer descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) rewrite each affected array from element zero, preserving unchanged descriptors. MoltenVK 1.4.2 writes auxiliary buffer lengths at index `i` instead of `start + i` for partial array updates ([MVKDescriptorSet.mm, BufferAuxSize](https://github.com/KhronosGroup/MoltenVK/blob/v1.4.2/MoltenVK/MoltenVK/GPUObjects/MVKDescriptorSet.mm#L1051)). Per-element writes therefore let one buffer length replace another even when Vulkan buffer handles and ranges are correct. @@ -181,7 +197,7 @@ - An [image table](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) whose record range overlaps a buffer or storage image written by its own dispatch faults (`table written by its dispatch`) when any entry is selected, even if the write misses the selected record. A selected column's range is its record and the bytes of the record before the descriptor. - An [image table](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) T# whose surface has no mapped byte faults when selected instead of becoming a zero texture as a direct T# does; what the console returns for it is not measured. - [User-data callees](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a compute shader's `s_swappc_b64` whose source pair is a user-data pair, or a copy of one made with `s_mov_b32` or `s_mov_b64` in the same block, calls the code at the address that user data holds. At dispatch the callee is read from guest memory up to its `s_setpc_b64` on the link pair, one copy of it is appended per call after the caller's last instruction, and the call is rewritten to `s_call_b64` to that copy. The words after the caller's last instruction are dropped from the recompiled code; in the shaders seen they are metadata or constants the caller reads relative to its program counter, and those reads go to the captured guest code, which linking does not change. A caller that jumps through a table among those words, which the control flow analysis reads from the recompiled code, throws. A copy runs at another program counter than the callee, so a callee that reads its program counter, makes a call, ends the program, jumps through another pair, returns before code it branches to, spans more than 64 KiB or is not readable throws. Callees are read again whenever a dispatch misses the dispatch cache; a dispatch that hits it reuses the linked program, so a title that rewrote a callee in place at the same address would keep running the old code. -- [Table callees](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a compute shader's `s_swappc_b64` whose source pair is two consecutive dwords of an `s_buffer_load_dword*`, or a copy of them made with `s_mov_b32` or `s_mov_b64` in the same block (a conditional branch forward past the call between them is crossed), calls one of the targets that buffer holds when the load's buffer descriptor comes from an `s_load_dwordx4` without SOFFSET from a user-data pointer, or from a pointer pair that two `s_mov_b32` of a literal or inline constant set, and the load's SOFFSET from an `s_lshl_b32` by 2 to 24 bits. At dispatch the descriptor and the buffer are read from guest memory, and every non-zero 64-bit value at the loaded offset plus a multiple of the shift's stride, below the descriptor's size (stride times num_records, or num_records bytes with stride 0), is read as a user-data callee and appended once per call. The call becomes a branch to a chain that compares the source pair with each target (`s_cmp_lg_u32` with a literal and `s_cbranch_scc1` per dword) and `s_call_b64`s the copy that matches; no match runs the original `s_swappc_b64` as an unlinked call. There is one chain for each value SCC has at the call, and the chain sets SCC back to that value (`s_cmp_eq_u32 0, 0` or `s_cmp_lg_u32 0, 0`) before the callee or the unlinked call runs. The searched range is the descriptor's whole size, which covers the range the load can reach whether it is checked in bytes or in strides; which one the PS5 uses for `s_buffer_load` was not measured. More than 64 distinct targets, a buffer above 64 MiB or a copy out of `s_call_b64` range throws. As with user-data callees, the buffer is read again only when a dispatch misses the dispatch cache, so a target a title adds to the buffer later is an unlinked call when a cached program runs. +- [Table callees](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a compute shader's `s_swappc_b64` whose source pair is two consecutive dwords of an `s_buffer_load_dword*`, or a copy of them made with `s_mov_b32` or `s_mov_b64` in the same block (a conditional branch forward past the call between them is crossed), calls one of the targets that buffer holds when the load's buffer descriptor comes from an `s_load_dwordx4` without SOFFSET from a user-data pointer, or from a pointer pair that two `s_mov_b32` of a literal or inline constant set, and the load's SOFFSET from an `s_lshl_b32` by 2 to 24 bits. The descriptor load may be in an earlier block, such as before a per-lane loop around the call, when it is the only write of the descriptor registers that reaches the buffer load on every path back from it, and no `s_call_b64` or statically resolved call lies on those paths; the paths may run through any `s_swappc_b64` that is not resolved statically, the table call itself included, whose callee is assumed to keep the descriptor registers. The chain compares the target each iteration actually loads, so a callee that changes them can only make a later target miss the chain and fault as an unlinked call. Paths follow fall-through, direct branches and the `s_setpc_b64` jumps whose target the control flow analysis resolves. A path that reaches, before a write of the descriptor registers, the program's first instruction, the entry of an in-program function (an `s_call_b64` target, whose callers are not followed), or an instruction that nothing modelled jumps or falls into fails the search. At dispatch the descriptor and the buffer are read from guest memory, and every non-zero 64-bit value at the loaded offset plus a multiple of the shift's stride, below the descriptor's size (stride times num_records, or num_records bytes with stride 0), is read as a user-data callee and appended once per call. The call becomes a branch to a chain that compares the source pair with each target (`s_cmp_lg_u32` with a literal and `s_cbranch_scc1` per dword) and `s_call_b64`s the copy that matches; no match runs the original `s_swappc_b64` as an unlinked call. There is one chain for each value SCC has at the call, and the chain sets SCC back to that value (`s_cmp_eq_u32 0, 0` or `s_cmp_lg_u32 0, 0`) before the callee or the unlinked call runs. The searched range is the descriptor's whole size, which covers the range the load can reach whether it is checked in bytes or in strides; which one the PS5 uses for `s_buffer_load` was not measured. More than 64 distinct targets, a buffer above 64 MiB or a copy out of `s_call_b64` range throws. As with user-data callees, the buffer is read again only when a dispatch misses the dispatch cache, so a target a title adds to the buffer later is an unlinked call when a cached program runs. - [AGC FS table](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp) (libSceAgcDriver) - the driver maps 2 MiB at 0xF_E0000000 when it is created and fills the table that libSceAgc keeps at 0xF_E0040000 (six 16-byte V# slots, slot 0 describing eight 16-byte records) the way PS5 12.00 libSceAgcDriver and libSceAgc do, but every record points at one AnyPS5 routine that only returns (`s_setpc_b64` on the link pair). Sony's routines, which order work before a title's `ds_ordered_count`, are not modeled, and the other five slots stay zero. - [DB_RENDER_CONTROL resummarize draws](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - HTILE is not modeled, so a RESUMMARIZE_ENABLE draw without depth or stencil tests and without color writes changes no modeled state and draws nothing. A pure resummarize with pass-through D16/D32 depth, no depth writes or bounds test, no stencil plane, no color writes and no running pixel stage also draws nothing; other resummarize, copy or decompress draws throw. - [Comparison-sampled R32 float and R16 unorm textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) whose surface a storage image holds are sampled from a D32_SFLOAT or D16_UNORM image that the GPU refills from the storage image whenever its version moves, because Vulkan compares only depth formats. Only surfaces of at least 1 MiB (`APS5_DEPTH_COPY_MIN_KIB`) that are not 3D take the copy; smaller ones stay CPU snapshots compared against guest memory. With copies of small surfaces, one title hit an intermittent boot-time crash that was not explained. @@ -190,7 +206,8 @@ - [16-bit depth with stencil](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) is stored as `D24_UNORM_S8_UINT`, or `D32_SFLOAT_S8_UINT`, on a device without `D16_UNORM_S8_UINT` as an attachment. Depth comparisons, clears and the depth image see the wider format's precision, so results at the 16-bit quantization boundary can differ from the console; the constant depth bias is scaled by 256 for D24, and a non-zero constant on the D32 float fallback throws. Not measured on console hardware. - [Depth surfaces created over storage writes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): a depth surface created without HTILE copies its depth plane from the storage image at the plane's address, a pending one first, when that image has the plane's extent, one level, a 2D view or a 2D array view (its layer 0 is copied) and a 16-bit format for a 16-bit plane or a 32-bit format for a 32-bit plane. A multisampled surface, a surface the device stores in another format, a surface with HTILE and a surface without such an image start from `DB_DEPTH_CLEAR`, and the stencil plane from `DB_STENCIL_CLEAR`; CPU writes to the plane's memory are not read. Not measured on the console, where the plane is that memory. - [Flip packets raise the graphics end-of-pipe event](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) (libSceAgcDriver) - a flip delivers the queue's end-of-pipe event (`sceAgcDriverAddEqEvent`) once the work recorded before it completes, as a `RELEASE_MEM` with `INT_SEL` does, but only on a queue that has never delivered an interrupt from such a release: the queue is latched the first time a release interrupt reaches the event queue, and flips on that queue deliver nothing while its own release interrupts keep arriving. The latch is cleared when the queue has flipped five times without another interrupt from its own releases: the fifth flip delivers the event and unlatched the queue, so a title that waits on the flip event after latching recovers only if it submits further flips. The bound cannot be by time: the next release interrupt can arrive seconds later than a suppressed flip across loading stalls, and delivering an unexpected flip event on a queue that pairs flips with releases can crash; no fixed window short enough to unblock a waiting title keeps such a title alive. Whether the console raises the event from the flip itself or from an interrupt its flip sequence contains is not measured; nor is how the console behaves for a title that waits on the flip event and also raises release interrupts on the same queue. -- [Host import windows and budget](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): a registered range larger than 64 MiB is imported one 64 MiB window at a time for buffer and fill accesses; an access across window edges imports the cells it touches, grown to the union of the range's imports it overlaps. An address-based build (`AcquireRegistered`) imports the range whole and replaces its windows. Live imports stay under a budget: `APS5_HOST_IMPORT_MIB`, else the larger of 6 GiB and half the physical memory, at most the largest host heap less the larger of 2 GiB and half that heap (imports, type-3 mirrors and buffers and textures placed in host memory share it; on a 16 GiB machine whose host heap is half the memory, a 2 GiB reserve left too little for the rest). A new import beyond it retires the least recently used imports not used in the last 256 lookups; one that would not fit even then is refused before anything is retired, and imports made when a range is mapped never evict. A heap mirror, sparse or not, whose range has left the write watch is refreshed in full at each use, every block counted as written; a build plans its range again only once the import path takes the range. A refused import is tried again at its next use. The 64 MiB window, the 256-lookup protection and the 2 GiB reserve are choices, not measured. +- [Host import windows and budget](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): a registered range larger than 64 MiB is imported one 64 MiB window at a time for buffer and fill accesses; an access across window edges imports the cells it touches, grown to the union of the range's imports it overlaps. An address-based build (`AcquireRegistered`) imports the range whole and replaces its windows. Live imports stay under a budget: `APS5_HOST_IMPORT_MIB`, else the larger of 6 GiB and half the physical memory, at most the largest host heap less the larger of 2 GiB and half that heap (imports, type-3 mirrors and buffers and textures placed in host memory share it; on a 16 GiB machine whose host heap is half the memory, a 2 GiB reserve left too little for the rest). Where the device enables `VK_EXT_memory_budget`, that host heap's reported budget less its other usage and an eighth, in 64 MiB steps, read at most once a second, also bounds the default budget once an import's `vkAllocateMemory` has failed; a set `APS5_HOST_IMPORT_MIB` is never lowered by the report. The report's other usage includes the host-visible copies and heap mirrors that stand in for refused imports, so it grows with the refusals; the first time the report would lower the budget, `[gpu] host import budget: host heap ...` prints the heap's reported budget and usage and the live imports. An import's `vkAllocateMemory` failure within an eighth of the budget, or the third in a row, lowers the budget to the live imports until the device or the configured budget changes; an isolated failure further below only marks its range failed. Live imports are not capped by `maxMemoryAllocationCount`: NVIDIA's Windows driver reports 4096, while Village held 27,167 live imports without a failure. A new import beyond it retires the least recently used imports not used in the last 256 lookups; one that would not fit even then is refused before anything is retired, and imports made when a range is mapped never evict. A heap mirror, sparse or not, whose range has left the write watch is refreshed in full at each use, every block counted as written; a build plans its range again only once the import path takes the range. A request that may not evict (an address-based build, an import when a range is mapped) remembers a refused range or block, with the live bytes and the budget at the refusal, and is answered from that memory, still counted as refused, until live imports drop below those bytes, the budget changes or one of its ranges changes. A request that may evict remembers its refusal too, answering later requests for the same run from memory with the room the refusal left (a request whose range fits in that room steps down to a smaller block without scanning the imports again, and a refusal that stepped down is remembered for each block it tried), while no import was made or retired, the budget has not grown, the import window and the run are unchanged, and no import can have become evictable since: the use count stays below the first use at which an import that was neither evictable nor held could leave the 256-lookup protection, and either the build floor has not risen or no held import could leave it yet. A join remembers its refusal the same way, apart from other requests for the same run, so a buffer bound across refused ranges does not try the join again at every upload. The shutdown line `[gpu] host imports at shutdown` gives the live, made, refused (and remembered) and failed counts per requester. The 64 MiB window, the 256-lookup protection, the 2 GiB reserve, the eighth, the 64 MiB steps and the three failures are choices, not measured. +- [Periodic pipeline cache saves skip busy intervals](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp) (libSceAgcDriver) - the saver thread snapshots the Vulkan pipeline cache only when no pipeline creation holds it (`TryLockForSnapshot`); otherwise it skips that 10 s interval. A blocking snapshot queued behind a long creation (an NVIDIA compile of a 1.5M-word compute pipeline runs for minutes) makes the writer-preferring `shared_mutex` hold every later pipeline creation on every queue until that compile ends, which froze Yōtei's (PPSA26344) loading at frame 797-799 for the whole compile. While creations overlap without a gap, no periodic save happens, so a crash in that stretch loses the pipelines built since the last save; `SaveNow` and the teardown save still wait for the lock. How often busy stretches skip saves in practice is not measured. - [Registration preparation at submit](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) - `Driver::Submit` waits for every registration-time shader preparation queued before the call, not only those of the shaders the commands use, so a submission behind a long queue of unrelated preparations waits for all of them on the submitting thread. Waiting there instead of in the queue worker keeps the GPU side of a submission short: a title that sees a submission take seconds treats the GPU as hung (Goat Simulator 3, PPSA04159, then stops submitting and its garbage collector fails a 10 s timeout). On the console shaders are compiled ahead of time, so neither wait exists; how long a title tolerates a slow submit call is not known. - [Resource cache entries holding released textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a sampled texture the texture cache drops while something else holds it, or makes without caching (`admitTexture` refusing a first use), is marked released, and `ResourceCache::Insert` refuses descriptor sets that hold one and removes the entries that do. The removal waits for the next insert: until then `Find` still returns those entries and their textures stay alive. One process-wide flag starts the sweep, so while the sampled cache is over its budget every insert scans all entries (1,024 at start, up to 16,384 after growth, or `APS5_RESOURCE_CACHE_ENTRIES`, each with its textures); that cost is not measured. When `ReleaseTextureCaches` runs on device memory exhaustion, the sampled textures these entries hold are only marked released and are freed at the next insert, so the immediate allocation retry gets no memory back from them. A draw or dispatch whose textures the cache refused builds its descriptor sets again on each use for as long as the cache stays full; the count bounds (`APS5_RESOURCE_CACHE_ENTRIES`, `APS5_RESOURCE_CACHE_TABLE_ELEMENTS`) still bound nothing by bytes, and sampled textures made outside the texture cache (the disabled cache, depth-surface copies) are never marked. Measured on one title on one RTX 4090: with the default bounds, sampled textures held outside the cache peaked at 2,836 MiB (7,830 MiB before) and device-local heap 0 stayed under its 22.9 GB budget for the whole run (26,357 MiB before, which aborted with `VK_ERROR_OUT_OF_DEVICE_MEMORY` at the wolf scene). Whether the extra descriptor-set builds cost frame time elsewhere is not measured. @@ -201,3 +218,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. 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. PPSA26344 reads a 3328x1872 R_X T# of 1537 slices this way: the whole array was a 12.4 GiB image that did not fit the device-local budget of an RTX 4090. +- [Sampled and storage views of 1 GiB or more](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) that stay whole are only logged (sampled views by `cachedTexture`, storage views by `cachedStorageTexture`), with the registered guest range holding their base and how many of their slices fit in it; nothing is capped to that range. PPSA26344 samples a 1467 MiB cube array window of which only 838 of 2934 slices lie inside its 3380 MiB range: the rest is read from whatever follows the range. Whether such slices are ever sampled is not measured. +- [Color export type against the target format](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - the export-format check refuses a `UINT16_ABGR` export (`SPI_SHADER_COL_FORMAT` 7) into a target whose color-buffer number format is not unsigned integer, but not the reverse. A 32-bit export (`32_R`, `32_GR`, `32_AR` or `32_ABGR`) into an unsigned-integer target leaves `uintExport` clear, so the recompiler declares the fragment output `float4`; `DecodeColorBuffer` maps a number format of 4 to `R32_UINT` from the color buffer alone, independent of the export format, so the float output reaches an integer attachment. Desktop drivers keep the raw bits, but the Khronos validation layer reports `Undefined-Value-ShaderFragmentOutputMismatch` and MoltenVK's Metal compiler refuses the pipeline. Which export format a title pairs with a 32-bit integer target is not measured, so the output type is left following the export format rather than the attachment. + +- [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..f8982027f0 100644 --- a/docs/dev/debt/libSceAjm.native.md +++ b/docs/dev/debt/libSceAjm.native.md @@ -2,7 +2,10 @@ ### 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. +- [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. +- [ATRAC9 vibration decoding](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native): channel configurations 6 (mono) and 7 (dual mono) use the pinned [vgmstream decoder](https://github.com/vgmstream/vgmstream/blob/7c520e8f9b078dfde2f84f1b5bcbad0c6c415806/src/coding/libs/atrac9_dec.c), including its 12/24/48 kHz profile limits. Synthetic fixtures cover interleaving, reset after malformed input, partial superframes and PCM16/PCM32/float output; console PCM rounding and physical haptic output are unverified. Ordinary channel configurations and discrete multichannel streams still use LibAtrac9. Decoder creation and decoding run under the existing AJM instance lock because the dependency rebuilds shared Huffman tables during initialization. - [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 - [sceAjmDecMp3ParseFrame](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - signature, result layout and rate tables taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation of the PS4 library, not confirmed on a PS5 title: the layer field is not checked and MPEG-2.5 stops at 64 kbps; the original file length (`parse_ofl`) layout (Xing/Info + LAME tag, VBRI, Fraunhofer `0xB4` block with its CRC, encoder delay = samples per frame + LAME delay + 529) also comes from shadPS4 only, and the Fraunhofer block has no other public description @@ -18,6 +21,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/libSceAjmi.md b/docs/dev/debt/libSceAjmi.md new file mode 100644 index 0000000000..d3ac224c47 --- /dev/null +++ b/docs/dev/debt/libSceAjmi.md @@ -0,0 +1,5 @@ +# libSceAjmi technical debt + +### Unknown function info + +- [libSceAjmi pending exports](../../../core/libs/prx/libSceAjmi/Export.cpp) - 5 named callable exports were identified in the PS5 3.20 export table and cross-checked against firmware 13.60. Signatures and behavior remain unverified; declarations use the unknown-signature convention and throw through `NotImplemented_nid_no_patch`. Evidence: [3.20 export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAjmi.c). diff --git a/docs/dev/debt/libSceAmpr.md b/docs/dev/debt/libSceAmpr.md index a9c31f321e..764ea7ec20 100644 --- a/docs/dev/debt/libSceAmpr.md +++ b/docs/dev/debt/libSceAmpr.md @@ -2,6 +2,7 @@ ### Unknown function info +- [AMPR command buffer null arguments](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - the result for a null command buffer is unknown for `sceAmprCommandBufferConstructor`, `sceAmprAprCommandBufferConstructor`, `sceAmprCommandBufferReset` and `sceAmprCommandBufferSetBuffer`; these return `SCE_KERNEL_ERROR_EINVAL` here, but the behavior has not been confirmed on a PS5. - [AMPR WriteAddressFromCounterPairOnCompletion](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - the pair of an even counter n is assumed to give counter n in the low 32 bits and n + 1 in the high 32 bits. Wait compare values, wait flush and counter index ranges follow the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation; the flush is accepted and has no effect, as commands run in order - [AMPR command buffer offsets](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - recording with a current offset greater than the bound memory size throws before calculating remaining capacity or writing a command. This validates the emulator's own command-buffer encoding; the console's response to a corrupted opaque buffer object is unknown. - [AMPR command sizes](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - commands use the emulator's own encoding with an 8-byte header, so `Nop(n)` takes 8 + 4n bytes where the console record is n dwords including its header (`NopWithData(n)`: (n + 1) dwords). The measure functions agree with the appended sizes, but a title that pads to an exact offset or the end of the buffer with `Nop(remaining / 4)` gets `SCE_KERNEL_ERROR_EBUSY` or a shifted offset. The at-start flag of the `_04_00` writes is ignored; commands run in order, each after the previous one completes @@ -12,6 +13,5 @@ ### Functional - [sceAmprAmmGetUsageStatsData](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) fills the caller-sized 24-byte statistics structure with 512 page-table pool entries, no allocated entries, a zero high watermark and ring idle flags 0x7, the constants of [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libAmpr.cpp); they are not measured on a console, and the real call is a pass-through to `sceKernelMapperGetUsageStatsData` per [drakmor/ampr_emu](https://github.com/drakmor/ampr_emu/blob/main/src/ampr_emu_amm.cpp), which this port has no kernel-side counterpart for. -- [Dinkumware thread exports](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) follow the Dinkumware threads library shipped with [Visual C++ 2013](https://github.com/adityaramesh/concurrency_runtime/tree/master/VS2013_SP2/crt/src/thr) (`cthread.c`) and its `atomic.c`, [microsoft/STL `cthread.cpp`](https://github.com/microsoft/STL/blob/main/stl/src/cthread.cpp) and C11 Annex K, without console verification. `_Thrd_t` is taken to be the guest `pthread_t`, as the `std::_Pad::_Launch(pthread**)` export suggests: `_Thrd_join` returns `_Thrd_error` (4) when `pthread_join` fails and stores the thread's return value truncated to `int`. `_Thrd_join` (32-bit store of the result, 0 or 4) and the spin lock (byte exchange loop, no yield) match that disassembly. The shared_ptr spin lock is one process-wide flag that yields while it spins. `wcstombs_s` is `wcsrtombs_s` with a fresh conversion state, so `*retval` excludes the terminating null as in Annex K, not as in the Microsoft CRT, and `_TRUNCATE` has no meaning. -- [_Cnd_signal, _Cnd_init_with_name](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - take a pointer to the handle slot like the libc `_Cnd_*` exports from [#3079](https://github.com/boykopovar/AnyPS5/pull/3079) and act on the same conditions. `_Cnd_signal` wakes one waiter and returns `_Thrd_success` (0), as `_Cnd_broadcast` does; whether the console maps a failing internal signal to `_Thrd_error` is not known. The signature `_Cnd_init_with_name(_Cnd_t*, const char* name)` is inferred from [fpPS4 `ps4_mtx_internal.pas`](https://github.com/red-prig/fpPS4/blob/trunk/src/libcinternal/ps4_mtx_internal.pas), where the PS4 `_Mtx_init` is `_Mtx_init_with_name` with the name `SceLibcI_Mtx` and the name only reaches the pthread object; here it is `_Cnd_init` and the name, which may be null, is ignored. Neither is confirmed on a PS5 `libc.prx`. `_Thrd_id` still throws: microsoft/STL returns an `unsigned int` thread id, but whether the PS5 returns the `pthread_t` or a kernel thread id, and at what width, is not known -- [std::_Pad](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) follows Dinkumware V6.00 `thr/xthread` and `thr/threadcall.cpp` of Visual C++ 2013 ([crt.vc12 `f1d47126`](https://github.com/ganboing/crt.vc12/tree/f1d47126/src/thr)), without console verification. The pad is taken to be that header's class with the console's pointer handles: a vtable pointer whose first slot is the pure virtual `_Go()`, then `_Cnd_t` at 0x8, `_Mtx_t` at 0x10 and `bool _Started` at 0x18, 32 bytes, the derived `_LaunchPad` target following it; the console's `_Pad(const char*)` and `_Launch(const char*, pthread**)`/`_Launch(pthread_attr**, pthread**)` overloads suggest named primitives and could mean another layout or vtable. `_Launch` starts the thread with `pthread_create` and default attributes (the console's `_Thrd_start` name, stack size and priority are unknown), its entry calls `_Go()` and returns the result as the exit code, and it does not run `_Cnd_do_broadcast_at_thread_exit`, which is absent here. A failing primitive throws the guest `std::system_error` that `_Throw_C_error` picks for its code, and a failing thread start the `resource_unavailable_try_again` one of `_Thrd_startX`; any `pthread_create` error counts, since the console's `_Thrd_start` result is unknown. A null pad or thread slot throws +- [Dinkumware thread exports](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) follow the Dinkumware threads library shipped with [Visual C++ 2013](https://github.com/adityaramesh/concurrency_runtime/tree/master/VS2013_SP2/crt/src/thr) (`cthread.c`) and its `atomic.c`, [microsoft/STL `cthread.cpp`](https://github.com/microsoft/STL/blob/main/stl/src/cthread.cpp) and C11 Annex K, without console verification. The spin lock (byte exchange loop, no yield) matches that disassembly. The shared_ptr spin lock is one process-wide flag that yields while it spins. `wcstombs_s` is `wcsrtombs_s` with a fresh conversion state, so `*retval` excludes the terminating null as in Annex K, not as in the Microsoft CRT, and `_TRUNCATE` has no meaning. +- [_Cnd_init_with_name](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - takes a pointer to the handle slot like the libc `_Cnd_*` exports from [#3079](https://github.com/boykopovar/AnyPS5/pull/3079) and acts on the same conditions. The signature `_Cnd_init_with_name(_Cnd_t*, const char* name)` is inferred from [fpPS4 `ps4_mtx_internal.pas`](https://github.com/red-prig/fpPS4/blob/trunk/src/libcinternal/ps4_mtx_internal.pas), where the PS4 `_Mtx_init` is `_Mtx_init_with_name` with the name `SceLibcI_Mtx` and the name only reaches the pthread object; here it is `_Cnd_init` and the name, which may be null, is ignored. This is not confirmed on a PS5 `libc.prx`. 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..62bdd6066f 100644 --- a/docs/dev/debt/libSceAppContent.md +++ b/docs/dev/debt/libSceAppContent.md @@ -3,8 +3,10 @@ ### 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 + +- [libSceAppContent pending exports](../../../core/libs/prx/libSceAppContent/Export.cpp) - 17 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAppContent.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. ### Functional - [Additional content](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) is not installed or mounted: `sceAppContentAddcontMount` answers `NOT_FOUND` for every entitlement label and `sceAppContentAddcontUnmount` for every mount point, as no add-content mount is ever handed out; `sceAppContentGetAddcontInfo` answers `DRM_NO_ENTITLEMENT` for every label and `sceAppContentGetAddcontInfoList` reports 0 entries; `sceAppContentAddcontEnqueueDownload` answers `PARAMETER` for a null label and `NOT_FOUND` otherwise, since no download is ever queued; `sceAppContentAddcontDelete` answers `PARAMETER` for a null label and `NOT_FOUND` otherwise, since no additional content is ever installed. `sceAppContentGetAddcontDownloadProgress` answers `PARAMETER` for a null label or progress and `NOT_FOUND` otherwise, as [dlc_emu](https://github.com/drakmor/dlc_emu/blob/fcf69b16d46a9850578bce19dc2c1b4dc867cbb5/src/dlc_modules/dlc_content.cpp#L2328-L2341) does for a label that is not installed additional content, and never writes the progress. Its 16-byte layout (`DataSize`, then `DownloadedSize`, both 64-bit) follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/AppContent/AppContent.cs#L45-L54) and is unused here. The signatures of `sceAppContentAddcontEnqueueDownload` and `sceAppContentGetAddcontDownloadProgress` follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/AppContent/AppContent.cs#L140-L148), and the signature of `sceAppContentAddcontDelete` follows [dlc_emu](https://github.com/drakmor/dlc_emu/blob/fcf69b16d46a9850578bce19dc2c1b4dc867cbb5/src/dlc_modules/dlc_content.h#L91-L92); dlc_emu is not a console measurement and no console source gives the error codes, so the signatures and the error codes are unverified diff --git a/docs/dev/debt/libSceAt9Enc.md b/docs/dev/debt/libSceAt9Enc.md new file mode 100644 index 0000000000..0ceb0e614c --- /dev/null +++ b/docs/dev/debt/libSceAt9Enc.md @@ -0,0 +1,6 @@ +# libSceAt9Enc technical debt + +### Unknown function info + +- [libSceAt9Enc](../../../core/libs/prx/libSceAt9Enc/Export.cpp) (libSceAt9Enc, sysmodule id `0x000000d1`) has no encoder, so all five exports throw: `sceAt9EncClearContext`, `sceAt9EncCreateEncoder`, `sceAt9EncEncode`, `sceAt9EncFlush` and `sceAt9EncQueryMemSize`. PPSA03332 imports them and fails to load (`libSceAt9Enc.prx: cannot open shared object file`) when the library is absent. The names are from the aerolib NID list, and the name reads as the ATRAC9 encoder, unconfirmed +- [sceAt9Enc*](../../../core/libs/prx/libSceAt9Enc/Export.cpp) (libSceAt9Enc) - the signatures follow [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json), which gives `int32_t sceAt9EncClearContext(void *pAt9EncWorkArea, int32_t *pInternalResult)`, `int32_t sceAt9EncCreateEncoder(void *pAt9EncWorkArea, struct *pAt9EncParam, int32_t memSize, int32_t *pInternalResult)`, `int32_t sceAt9EncEncode(void *pAt9EncWorkArea, const void *pInputBuf, void *pOutputBuf, uint32_t inputBufSize, uint32_t outputBufSize, uint32_t *pOutputDataByte, uint32_t *pUsedBufSize, int32_t *pInternalResult)`, `int32_t sceAt9EncFlush(void *pAt9EncWorkArea, void *pOutputBuf, uint32_t outputBufSize, uint32_t *pOutputDataByte, int32_t *pInternalResult)` and `int32_t sceAt9EncQueryMemSize(struct *pAt9EncParam, int32_t *pMemSize, int32_t *pInternalResult)`. Its `struct *pAt9EncParam` is a placeholder, so that parameter's layout is unknown diff --git a/docs/dev/debt/libSceAudio3d.md b/docs/dev/debt/libSceAudio3d.md index 09f5bce9c1..c7d94441a4 100644 --- a/docs/dev/debt/libSceAudio3d.md +++ b/docs/dev/debt/libSceAudio3d.md @@ -2,10 +2,31 @@ ### Silent stubs -- [libSceAudio3d](../../../core/libs/prx/libSceAudio3d/Export.cpp) - the port produces no sound: objects can be reserved and `sceAudio3dObjectSetAttributes` accepts their attributes, but the library has no bed exports or mixing to give them audio, the attributes have no effect and `sceAudio3dPortPush` only paces the queue at `granularity` samples per frame at 48 kHz. `sceAudio3dPortSetAttribute` accepts the late reverb level and the downmix spread attributes and ignores them. `sceAudio3dObjectSetAttributes` validates the port, the reserved object and the attribute array (an object id that was never reserved returns the invalid-object error, while shadPS4 returns success for it and the error only for a released object; neither is measured on a console), treating the reset-state attribute as valid with a null value and requiring a value for the others; the attribute identifiers and the value of each are not interpreted. `sceAudio3dPortFlush` drops the frames queued by `sceAudio3dPortAdvance` instead of mixing and submitting them to an output, as there is no output +- [libSceAudio3d](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - `sceAudio3dObjectSetAttributes` validates the port, the reserved object and the attribute array (an object id that was never reserved returns the invalid-object error, while shadPS4 returns success for it; neither is measured on a console), treating the reset-state attribute as valid with a null value and requiring a value for the others, but the identifiers and the value of each are not interpreted, so an object has no samples, no gain and no position: the ids of the `PCM`, `GAIN` and `POSITION` attributes and the `POSITION` layout are unknown (the aerolib NID list gives the names only, the Orbital IDA type database the signatures only and shadPS4's values are PS4). `sceAudio3dPortSetAttribute` accepts the late reverb level and the downmix spread attributes and ignores them, and `sceAudio3dPortFlush` drops the frames queued by `sceAudio3dPortAdvance` instead of mixing and submitting them to an output + +### Functional + +- [libSceAudio3d bed path](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - bed PCM is mixed and played: `sceAudio3dBedWrite` and `sceAudio3dBedWrite2` queue a bed, bounded by `queue_depth` and `NOT_READY` when full, `sceAudio3dPortAdvance` consumes one and mixes it into a stereo frame with a linear-balance pan and a 5.1/7.1 downmix, and an advance with no bed queued mixes silence ([Mixer.hpp](../../../core/libs/prx/libSceAudio3d/Mixer.hpp)) and queues the frame, and `sceAudio3dPortPush` opens a type-126 AudioOut port on first use and submits the queued frames to it. The explicit `sceAudio3dAudioOutOpen`, `sceAudio3dAudioOutOutput` and `sceAudio3dAudioOutOutputs` exports queue their blocks and the port drains them. The attribute enum values and the panning model are unverified ### Unknown function info - [sceAudio3dPortOpen](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - buffer modes 0 and 1 (also selected by the 0x10 and 0x18 parameter sizes) and 3 beds depend on an SDK version check in the module ([prosper](https://github.com/mattias800/prosper)) and throw. A repeated `sceAudio3dInitialize` (`NOT_READY` in shadPS4, 0 in prosper) and `sceAudio3dPortAdvance` on a full queue (`NOT_READY` in shadPS4, unhandled in KytyPS5) throw too - [sceAudio3dPortGetAttributesSupported](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - the supported list (late reverb level, downmix spread radius, downmix spread height aware) is inferred from the attributes `sceAudio3dPortSetAttribute` accepts; its order and the count written when the caller capacity is smaller are unverified. [shadPS4](https://github.com/shadps4-emu/shadPS4) returns object attributes (PCM, gain...) instead, prosper and KytyPS5 have no body - [sceAudio3dObjectReserve](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - object ids, the `max_objects` limit and the checks of `sceAudio3dObjectReserve` and `sceAudio3dObjectUnreserve` follow [shadPS4](https://github.com/shadps4-emu/shadPS4) (PS4), the only reference with a body: ids count up from 1 per port, skipping 0, 0xFFFFFFFF and ids in use, and a failed reserve writes 0xFFFFFFFF +- [sceAudio3dStrError](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dPortOpenEx](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dPortCreate](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dPortDestroy](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dPortGetStatus](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dReportRegisterHandler](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dReportUnregisterHandler](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dGetSpeakerArrayMemorySize](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dCreateSpeakerArray](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dDeleteSpeakerArray](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dGetSpeakerArrayMixCoefficients](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dGetSpeakerArrayMixCoefficients2](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [Audio3d bed downmix coefficients](../../../core/libs/prx/libSceAudio3d/Mixer.hpp) (libSceAudio3d) - the centre and the surrounds fold into stereo with a 0.7071 gain and the LFE channel is dropped, which follows [shadPS4](https://github.com/shadps4-emu/shadPS4) (PS4): its `DOWNMIX_CENTER` and `DOWNMIX_SURROUND` are 0.7071 and its `DOWNMIX_LFE` is 0, so the `if constexpr` that adds the LFE is compiled out. [prosper](https://github.com/mattias800/prosper) disagrees on the LFE only: it folds it into both sides with a 0.5 gain, in its AudioOut2 main bed path, which is not an Audio3d path. KytyPS5 has no Audio3d mixing at all. The absolute level is unmeasured: fpPS4 scales the front by 1 + 3/sqrt(2) instead of 1 +- [Audio3d bed channel order](../../../core/libs/prx/libSceAudio3d/Mixer.hpp) (libSceAudio3d) - the buffer is read as L, R, C, LFE, Ls, Rs, Lrs, Rrs, the standard order, which three references agree on: [shadPS4](https://github.com/shadps4-emu/shadPS4) reads the front, centre, LFE and surround samples at those indices, [prosper](https://github.com/mattias800/prosper) documents FL, FR, FC, LFE and then surround pairs, and fpPS4 names them `_FL=0 _FR=1 _FC=2 _LF=3 _SL=4 _SR=5 _BL=6 _BR=7`. fpPS4's `STD` layout swaps the side and back pairs, which this library does not model +- [sceAudio3dAudioOutOutput](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - the return is the number of frames per channel in the queued block, which follows [KytyPS5](https://github.com/KytyPS5/KytyPS5) and [prosper](https://github.com/mattias800/prosper), whose `sceAudioOutOutput` returns the grain, and this project's own `sceAudioOutOutput`, which returns the port's `samplesNum` (the buffer length in frames). [shadPS4](https://github.com/shadps4-emu/shadPS4) returns the total samples instead (`len * channels`) and fpPS4 returns 0; no console measurement settles it +- [sceAudio3dPortAdvance buffer mode](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - a mode other than 2 returns `NOT_SUPPORTED` (0x80EA0008), the value [shadPS4](https://github.com/shadps4-emu/shadPS4) uses for it and the one [prosper](https://github.com/mattias800/prosper) reads from the PS5 module's own error table, where 0x80EA0008 is what the per-port calls return for a buffer mode they do not support. shadPS4 and prosper refuse only mode 0 in `sceAudio3dPortAdvance` and modes 0 and 1 in `sceAudio3dPortPush`, while [KytyPS5](https://github.com/KytyPS5/KytyPS5) requires mode 2 at `sceAudio3dPortOpen` and halts otherwise, so this library refuses every other mode at the open and the check here is unreachable. 0x80EA0005 is `OUT_OF_MEMORY`, not `NOT_SUPPORTED` +- [sceAudio3dPortAdvance on a full queue](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - a full queue returns `NOT_READY` (0x80EA0007) instead of throwing, which follows [shadPS4](https://github.com/shadps4-emu/shadPS4), the only reference with a body: it returns `ORBIS_AUDIO3D_ERROR_NOT_READY` when the mixed queue is at the port's depth. [KytyPS5](https://github.com/KytyPS5/KytyPS5) halts instead and prosper has no queue at all diff --git a/docs/dev/debt/libSceAudioIn.md b/docs/dev/debt/libSceAudioIn.md index 99e0629243..04f555333b 100644 --- a/docs/dev/debt/libSceAudioIn.md +++ b/docs/dev/debt/libSceAudioIn.md @@ -6,8 +6,41 @@ ### Unknown function info -- [sceAudioInIsPadEmbeddedMic](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - name from its NID (`X+4jdIS75P0`); signature `int32_t(int32_t handle)` from the Ghost of Yotei (PPSA26344) call site in #2355; the result's meaning is unconfirmed, so it still throws rather than invent one +- [sceAudioInIsPadEmbeddedMic](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - name from its NID (`X+4jdIS75P0`); signature `int32_t(int32_t handle)` from the PPSA26344 call site in #2355; the result's meaning is unconfirmed, so it still throws rather than invent one +- [sceAudioInDeviceIdOpen](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInDeviceIdAsyncOpen](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetUsbGain](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInIsSharedDevice](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInDeviceIdHqOpen](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInDeviceHqOpen](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetConnections](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetPortConnections](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetPortStatuses](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInGetHandleStatusInfo](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInOpenEx](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInHqOpenEx](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInDeviceOpenEx](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInChangeAppModuleState](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetSparkParam](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetSparkSideTone](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetCompressorPreGain](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetNoiseGateThreshold](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetMicLevel](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetDeviceMuteStatus](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetDevConnection](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInCountPorts](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetUserMute](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInVmicCreate](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInVmicDestroy](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInVmicWrite](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInVmicGetStatus](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInVmicSetMute](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetMode](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetMode2](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInExtCtrl](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInInputs](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature ### 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..f6e6901f1f 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -24,6 +24,90 @@ - [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 +- [sceAudioOutOpenEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOutSetSystemDebugState](../../../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 +- [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 +- [sceAudioOut2LoPortGetStateEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2LoTerminate](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) throws. It takes no arguments and returns an int, and stops the low-latency service ([SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut2.cs#L497-L498)), but what it does to open contexts and ports and what it returns when the service is not started are unknown, and its counterpart `sceAudioOut2Terminate` is unimplemented. +- [sceAudioOut2ReportRegisterHandler](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2ReportUnregisterHandler](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2GetTvCorrectionInfo](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2GetIpcHandle](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2GetSystemStateEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2MasteringInitEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2SetDebugHeadphoneMode](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2DebugStateCtrl](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2ContextBedWrite](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2PortCreateEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [sceAudioOut2InitializeEx](../../../core/libs/prx/libSceAudioOut/src/Unimplemented.cpp) (libSceAudioOut) - unknown signature +- [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 @@ -31,4 +115,5 @@ - [sceAudioOut2Set3DLatency](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) and [sceAudioOut2MasteringInit, sceAudioOut2MasteringTerm and sceAudioOut2MasteringSetParam](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) have no effect: there is no 3D rendering or mastering stage. SetParam accepts the parameter set, and SetSystemDebugState records the debug state id, without anything to apply them to. - [AudioOut2 contexts](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) with an SDL device and no `sceAudioOut2ContextAdvance` calls read a grain's port buffers at the next push, at least 100 us after the grain was pushed, and play the mix behind a 40 ms cushion that the queue level leaves out. When the hardware reads the buffers is not known. The timing follows a title whose mixer fills a buffer after pushing it, reuses it two pushes later, and may still be writing it when the next push follows within microseconds. The last grain pushed before a title stops pushing is never read or played. - [sceAudioOut2ContextGetQueueLevel](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) with an SDL device counts the grains that `sceAudioOut2ContextAdvance` completed and no push has consumed as queued, together with the pushed grains beyond the 40 ms cushion, up to `queue_depth`. Whether the console counts them is unknown. Pushes that fill the cushion leave the level at 0, so a title that renders as many grains as the level leaves room for and pushes them on another thread rendered a full queue again after each of those pushes: PPSA23226's Wwise sink passes the available count to the sound engine as the number of frames to render, advances once per frame and pushes on a separate thread, and exceeded 16 completed grains. Counted, the completed grains keep such a title at most `queue_depth` grains ahead of its pushes. +- [sceAudioOut2LoInit, sceAudioOut2LoContext* and sceAudioOut2LoPort* calls](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Low.cpp) (libSceAudioOut) have no low-latency path: each forwards to its `sceAudioOut2` counterpart, so grain and queue depth are the context's `num_grains` and `queue_depth`, and playback keeps the 40 ms cushion. [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut2.cs#L489-L545) only says each call takes the same handles and parameter blocks as its counterpart and drives the low-latency clock with a shallow queue and a small grain ([docs](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/docs/audio.md#L662-L668)). Unverified: the console's grain and depth limits, its error for a handle of the other service (accepted here) and what `sceAudioOut2LoInit` returns when repeated (0 here). - [sceAudioOutSysGetHdmiMonitorInfo](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) reports an HDMI sink without a monitor name or audio descriptors: it zeroes the 0x180 bytes and returns 0, since audio reaches the host through SDL as PCM and there is no EDID to describe. The arguments `(type, info, size)`, the required size 0x180 and the fields titles read (monitor name at +0x0B, 13 bytes; descriptor count as a 32-bit value at +0x94; 8-byte descriptors `[coding type, channels, rate mask, 0, extra, 0, 0, 0]` from +0x98) come from [PS5-Audio-Passthrough-Research](https://github.com/sainsaji/PS5-Audio-Passthrough-Research/blob/c3be9c2afcb8f7838784ead0fe1c12a2ceea965a/docs/FINDINGS.md) (firmware 12.70, one monitor). What a console without an HDMI audio sink returns, an error or no descriptors, is unknown. The other bytes, among them the header at +0x90 (`01 09 4f 00` on that monitor), have no known meaning and are written as 0. No LPCM descriptor is reported for the host device, as the rate mask and extra byte of one are not known. A null buffer returns `0x80260004` like the other AudioOut calls; the console's code is not measured. Monitor type 0x10, which the console also accepts, other types and sizes other than 0x180 throw: their results and error codes are unknown. diff --git a/docs/dev/debt/libSceAudioPropagation.md b/docs/dev/debt/libSceAudioPropagation.md index 3cda9322dc..30802ce4c7 100644 --- a/docs/dev/debt/libSceAudioPropagation.md +++ b/docs/dev/debt/libSceAudioPropagation.md @@ -2,7 +2,7 @@ ### Silent stubs -- [libSceAudioPropagation](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) implements only the no-acoustic-propagation path: the system keeps a 16-byte header in the title's CPU memory (no GPU memory), rooms, portals, sources and materials are tracked handles, attributes are validated and have no effect, no rays are requested and sources report no audio paths. Ray results, path calculation and audio paths are accepted only when empty and throw otherwise; `sceAudioPropagationSourceRender` writes silence (zeroes) to each source's output buffer for the given size, without a dry signal. Objects still alive when their system is destroyed become unusable, and their later destroy or unregister is a no-op: PPSA21567 releases its reference-counted room, portal and source owners just before `sceAudioPropagationSystemDestroy`, so the order for every object is not proven. Occlusion, reflections and reverb from level geometry are absent; in-game behaviour remains unverified. +- [libSceAudioPropagation](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) implements only the no-acoustic-propagation path: the system keeps a 16-byte header in the title's CPU memory (no GPU memory), rooms, portals, sources and materials are tracked handles, attributes are validated and have no effect, no rays are requested and sources report no audio paths. Ray results, path calculation and audio paths are accepted only when empty and throw otherwise; `sceAudioPropagationSourceRender` preserves each destination mix because there are no path contributions, following the additive-output contract in [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioPropagation.cs); console behaviour has not been measured. It adds no dry signal. Objects still alive when their system is destroyed become unusable, and their later destroy or unregister is a no-op: PPSA21567 releases its reference-counted room, portal and source owners just before `sceAudioPropagationSystemDestroy`, so the order for every object is not proven. Occlusion, reflections and reverb from level geometry are absent; in-game behaviour remains unverified. ### Unknown function info diff --git a/docs/dev/debt/libSceAudiodec.md b/docs/dev/debt/libSceAudiodec.md index 0e37f5e79f..15c8c1cbe1 100644 --- a/docs/dev/debt/libSceAudiodec.md +++ b/docs/dev/debt/libSceAudiodec.md @@ -8,4 +8,4 @@ ### Functional - [sceAudiodecDecode](../../../core/libs/prx/libSceAudiodec/src/Codecs.cpp) (libSceAudiodec) - an ADTS stream with channel configuration 0 (layout in a program config element) is still parsed by FFmpeg, so `ui_enable_heaac` 0 does not remove its SBR and a mono one with Parametric Stereo comes out as stereo. -- [libSceAudiodec](../../../core/libs/prx/libSceAudiodec/Export.cpp) throws for the 24-bit PCM word size (`iBwPcm` 0): its sample layout is unknown. ATRAC9 decoding is covered only by configuration and error tests, as no ATRAC9 encoder is available for a fixture. +- [libSceAudiodec](../../../core/libs/prx/libSceAudiodec/Export.cpp) accepts only 16-bit PCM, and float PCM for ATRAC9 and M4AAC, as `sceAudiodecCreateDecoder` did in a payload process on a PS5 (firmware 13.60), which returned `ERROR_INVALID_WORD_LENGTH` for `iBwPcm` 0, 3 and -1 with each codec and for float with MP3; other word sizes are assumed to fail the same way. ATRAC9 decoding is covered only by configuration and error tests, as no ATRAC9 encoder is available for a fixture. diff --git a/docs/dev/debt/libSceAudiodec.native.md b/docs/dev/debt/libSceAudiodec.native.md new file mode 100644 index 0000000000..01bc5f3533 --- /dev/null +++ b/docs/dev/debt/libSceAudiodec.native.md @@ -0,0 +1,5 @@ +# libSceAudiodec.native technical debt + +### Unknown function info + +- [libSceAudiodec.native pending exports](../../../core/libs/prx/libSceAudiodec.native/Export.cpp) - 14 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAudiodec.native.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. diff --git a/docs/dev/debt/libSceAudiodecCpu.md b/docs/dev/debt/libSceAudiodecCpu.md new file mode 100644 index 0000000000..d249001e8d --- /dev/null +++ b/docs/dev/debt/libSceAudiodecCpu.md @@ -0,0 +1,5 @@ +# libSceAudiodecCpu technical debt + +### Unknown function info + +- [libSceAudiodecCpu pending exports](../../../core/libs/prx/libSceAudiodecCpu/Export.cpp) - 8 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAudiodecCpu.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. diff --git a/docs/dev/debt/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/libSceAvPlayer.md b/docs/dev/debt/libSceAvPlayer.md index 148f5db253..5a2f1941ed 100644 --- a/docs/dev/debt/libSceAvPlayer.md +++ b/docs/dev/debt/libSceAvPlayer.md @@ -15,6 +15,9 @@ - [sceAvPlayerInitEx](../../../core/libs/prx/libSceAvPlayer/Export.cpp) (libSceAvPlayer) - the PS5 init data layout (`this_size` 0x230: auto start at 0x74, nine 0x30-byte thread parameter blocks from 0x78, output video frame buffer count at 0x228) follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/avPlayer.cpp), [prosper](https://github.com/mattias800/prosper/blob/main/prosper/src/hle/video/avplayer.cpp), [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Media/AvPlayer.cs) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/AvPlayer/AvPlayerExports.cs), which agree on it. PPSA25063 fills the same fields, writes auto start 0 and starts playback with `sceAvPlayerStart`; a title that sets auto start has not been seen. A smaller `this_size` is read with the PS4 offsets. The thread parameters are ignored - [AvPlayer auto start](../../../core/libs/prx/libSceAvPlayer/src/Player.cpp) (libSceAvPlayer) - the auto start that `sceAvPlayerAddSource` queues for a player with auto start or without an event callback starts the player only while it is still ready: a title that starts the player itself before the auto start runs keeps its playback and the frames it holds. Not confirmed on a title. +- [Paused initial video](../../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) keeps the first picture available when audio has already established the playback epoch before the caller pauses. Further pictures of that epoch stay blocked, as after a paused jump. The console's exact initial preparation ordering has not been measured. + ### Functional +- [sceAvPlayerGetVideoData / sceAvPlayerGetVideoDataEx](../../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) - delivers a video frame up to 0.75 × frame duration ahead of its timestamp in default sync mode to accommodate presentation loops that poll slightly before the refresh boundary; the console's exact presentation tolerance is unverified. - [sceAvPlayerGetVideoData](../../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) in sync mode `SCE_AVPLAYER_AV_SYNC_MODE_NONE` skips queued frames that the audio clock has already passed while the title pulls audio, so a title that requests video slower than the film's frame rate keeps the picture with the sound. Whether the console hands out every frame in this mode is unknown. PPSA12544 requests 7-19 frames per second for a 30 fps cinematic diff --git a/docs/dev/debt/libSceAvPlayer.native.md b/docs/dev/debt/libSceAvPlayer.native.md new file mode 100644 index 0000000000..85b1b75aba --- /dev/null +++ b/docs/dev/debt/libSceAvPlayer.native.md @@ -0,0 +1,5 @@ +# libSceAvPlayer.native technical debt + +### Unknown function info + +- [libSceAvPlayer.native pending exports](../../../core/libs/prx/libSceAvPlayer.native/Export.cpp) - 2 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvPlayer.native.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. diff --git a/docs/dev/debt/libSceAvPlayerStreaming.md b/docs/dev/debt/libSceAvPlayerStreaming.md new file mode 100644 index 0000000000..4ef5238bac --- /dev/null +++ b/docs/dev/debt/libSceAvPlayerStreaming.md @@ -0,0 +1,7 @@ +# libSceAvPlayerStreaming technical debt + +### Unknown function info + +- [libSceAvPlayerStreaming pending exports](../../../core/libs/prx/libSceAvPlayerStreaming/Export.cpp) - 33 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvPlayerStreaming.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. + +- [libSceAvPlayerStreaming pending exports](../../../core/libs/prx/libSceAvPlayerStreaming/Export.cpp) - 33 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvPlayerStreaming.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. diff --git a/docs/dev/debt/libSceBeisobmf.md b/docs/dev/debt/libSceBeisobmf.md new file mode 100644 index 0000000000..643f09dd5e --- /dev/null +++ b/docs/dev/debt/libSceBeisobmf.md @@ -0,0 +1,7 @@ +# libSceBeisobmf technical debt + +### Unknown function info + +- [libSceBeisobmf pending exports](../../../core/libs/prx/libSceBeisobmf/Export.cpp) - 18 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceBeisobmf.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. + +- [libSceBeisobmf pending exports](../../../core/libs/prx/libSceBeisobmf/Export.cpp) - 18 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceBeisobmf.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. diff --git a/docs/dev/debt/libSceBemp2sys.md b/docs/dev/debt/libSceBemp2sys.md new file mode 100644 index 0000000000..1ca60eb890 --- /dev/null +++ b/docs/dev/debt/libSceBemp2sys.md @@ -0,0 +1,7 @@ +# libSceBemp2sys technical debt + +### Unknown function info + +- [libSceBemp2sys pending exports](../../../core/libs/prx/libSceBemp2sys/Export.cpp) - 31 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceBemp2sys.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. + +- [libSceBemp2sys pending exports](../../../core/libs/prx/libSceBemp2sys/Export.cpp) - 31 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceBemp2sys.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. diff --git a/docs/dev/debt/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/libSceCdlgPlayerReview.md b/docs/dev/debt/libSceCdlgPlayerReview.md index bdfe9125fe..98ff1e48e6 100644 --- a/docs/dev/debt/libSceCdlgPlayerReview.md +++ b/docs/dev/debt/libSceCdlgPlayerReview.md @@ -3,6 +3,7 @@ ### Unknown function info - [scePlayerReviewDialogOpen, scePlayerReviewDialogGetResult](../../../core/libs/prx/libSceCdlgPlayerReview/Export.cpp) (libSceCdlgPlayerReview) - the PS5-only dialog has no public reference: the parameter is not read and the result is assumed to start with the 32-bit common dialog result, as `sceWebBrowserDialogGetResult` assumes +- [scePlayerReviewDialogClose](../../../core/libs/prx/libSceCdlgPlayerReview/Export.cpp) (libSceCdlgPlayerReview) - `scePlayerReviewDialogClose` moves an initialized dialog straight to `COMMON_DIALOG_STATUS_FINISHED` even when called before Open, as Signin, NetCtlAp and PlayerSelection do; MsgDialog.native, SaveDataDialog.native and ErrorDialog return an error there instead. Unverified against real hardware ### Functional diff --git a/docs/dev/debt/libSceCesCs.md b/docs/dev/debt/libSceCesCs.md index 98512c9d7b..df97d9e096 100644 --- a/docs/dev/debt/libSceCesCs.md +++ b/docs/dev/debt/libSceCesCs.md @@ -4,8 +4,11 @@ - [sceCesMbcsUcsContextInit](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) stores the profile pointer in the first 8 bytes of the caller's context; the console's context layout is not known. It returns the invalid-parameter error for a null context. The signature `int sceCesMbcsUcsContextInit(union* context, const union* mbcsUcsPrf)` follows the [Orbital IDA type database](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json). `sceCesRefersUcsProfileCp1251` returns a profile accepted by `sceCesSbcToUtf8` and `sceCesUtf8ToSbc` only, with the Windows-1251 table of Microsoft's [bestfit1251.txt](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/bestfit1251.txt), where byte 0x98 (unassigned) maps to U+0098 like the unassigned Cp1252 bytes; the console behavior for that byte is not verified - [EUC-JP (CP51932) conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs): `sceCesUcsProfileInitEucJpCp51932` marks the caller's sheet with an 8-byte tag and returns it as the profile. `sceCesUtf8StrToMbcsStr`, `sceCesUcs2StrToMbcsStr`, `sceCesMbcsStrToUtf8Str`, `sceCesMbcsStrToUcs2Str`, their `Get*Len` forms and `sceCesEucJpToUcs2` convert ASCII, the JIS X 0201 half-width katakana (`0x8E` + `0xA1`-`0xDF`, U+FF61-U+FF9F) and the two-byte rows `0xA1`-`0xFE` for that profile, and every character for the CP1252 and CP1251 profiles. The two-byte table ([JisX0208.hpp](../../../core/libs/prx/libSceCesCs/JisX0208.hpp), 7336 of 8836 cells) is Microsoft code page 932 read through the standard JIS row/cell to Shift-JIS arithmetic, so it includes NEC row 13 and the NEC-selected IBM rows 89-92 and maps the Microsoft variants (U+FF5E at `0xA1C1`, U+2225, U+FF0D, U+FFE0-U+FFE2) instead of JIS X 0208's; that the console's CP51932 table equals it is assumed from the Microsoft code page, not measured. A character with several cells encodes to the lowest cell. A well-formed but unassigned cell answers `UNASSIGNED_CODE`, a trail byte outside `0xA1`-`0xFE` `INVALID_ENCODE`, a cut pair `SRC_BUFFER_END`; the console's errors for these are not known. Three-byte JIS X 0212 (`0x8F`) characters convert through a second table ([JisX0212.hpp](../../../core/libs/prx/libSceCesCs/JisX0212.hpp), 6067 of 8836 cells) read from the Unicode consortium's [JIS0212.TXT](https://unicode.org/Public/MAPPINGS/OBSOLETE/EASTASIA/JIS/JIS0212.TXT); that the console's CP51932 table equals it is assumed from that mapping, not measured. A well-formed but unassigned three-byte cell answers `UNASSIGNED_CODE` and a cut triple `SRC_BUFFER_END`. A character with cells in both JIS X 0208 and JIS X 0212 encodes to the two-byte form, and one only in JIS X 0212 to the three-byte form. Signatures follow [orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json). The string forms stop at the source limit or at a zero code unit, which is neither converted nor counted, and write no terminator; `*Len` outputs count code units consumed and produced. A code point outside the BMP answers `UNASSIGNED_CODE`. Unverified on hardware. -- [sceCesUcsProfileInitSJis1997Cp932](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - returns the sheet argument without writing profile tables to it, and returns null for a null pointer; Shift-JIS conversion tables are unimplemented. The signature `union* sceCesUcsProfileInitSJis1997Cp932(SceCesUcsProfileSheet* sheet)` follows the [Orbital IDA type database](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json) +- [Shift-JIS (CP932) conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs): `sceCesUcsProfileInitSJis1997Cp932` marks the caller's sheet with an 8-byte tag and returns it as the profile. `sceCesUtf8StrToMbcsStr`, `sceCesUcs2StrToMbcsStr`, `sceCesMbcsStrToUtf8Str`, `sceCesMbcsStrToUcs2Str`, and their `Get*Len` forms convert ASCII, JIS X 0201 half-width katakana (`0xA1`-`0xDF`, U+FF61-U+FF9F), two-byte Shift-JIS sequences (`0x81`-`0x9F` and `0xE0`-`0xFC` lead, `0x40`-`0x7E` and `0x80`-`0xFC` trail) mapped to JIS X 0208 row/cell indices against the CP932 table ([JisX0208.hpp](../../../core/libs/prx/libSceCesCs/JisX0208.hpp)), and the IBM extensions (`0xFA40`-`0xFC4B`, 388 characters) decoded to their corresponding JIS X 0208 code points. The user-defined area (`0xF040`-`0xF9FC`, which CP932 maps to Private Use Area U+E000-U+E757) and unassigned lead/trail combinations answer `UNASSIGNED_CODE`, an invalid lead or trail byte `INVALID_ENCODE`, a cut sequence `SRC_BUFFER_END`. Characters with multiple cells encode to the lowest cell. Signatures follow [orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json). Unverified on hardware. ### 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`) +- [libSceCesCs firmware 3.20 pending exports](../../../core/libs/prx/libSceCesCs/PendingExports.cpp) - 249 symbol names are present in [the PS5 3.20 libSceCesCs export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCesCs.c). Their signatures and console behaviour are unverified. The declarations use the existing no-parameter unknown-signature convention solely to resolve imports, and every call throws through `NotImplemented_nid_no_patch`. 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/libSceConvertKeycode.md b/docs/dev/debt/libSceConvertKeycode.md index 458e175688..706e31a66e 100644 --- a/docs/dev/debt/libSceConvertKeycode.md +++ b/docs/dev/debt/libSceConvertKeycode.md @@ -2,7 +2,7 @@ ### Unknown function info -- [sceConvertKeycodeGetCharacter](../../../core/libs/prx/libSceConvertKeycode/Export.cpp) (libSceConvertKeycode) - the signature `(uint16_t keycode, uint32_t, uint32_t keyboardType, char16_t* character)` is inferred from PPSA12361, which passes a 16-bit keycode, 0, the type from `sceConvertKeycodeGetImeKeyboardType` and a pointer it reads one UTF-16 unit from when the result is 0. The meaning of the second argument is unknown. PRAGMATA (PPSA02530, #5726) and Onimusha: Way of the Sword (PPSA27836) pass `0x4000000` as a constant: Onimusha calls it for every keycode from 0 to 0x8F to build a table of key names, upper-cases `a` to `z` itself and skips keycodes that fail or return 0, so that bit is ignored and the unshifted character is returned as for 0. Any other bit throws, and so does a keyboard type other than 0. The returned character for HID usage `0x64` has not been measured on a console; `\` is an unverified US-layout guess based on the Windows VK mapping, which does not establish the console result. +- [sceConvertKeycodeGetCharacter](../../../core/libs/prx/libSceConvertKeycode/Export.cpp) (libSceConvertKeycode) - the signature `(uint16_t keycode, uint32_t, uint32_t keyboardType, char16_t* character)` is inferred from PPSA12361, which passes a 16-bit keycode, 0, the type from `sceConvertKeycodeGetImeKeyboardType` and a pointer it reads one UTF-16 unit from when the result is 0. The meaning of the second argument is unknown. PPSA02530 (#5726) and PPSA27836 pass `0x4000000` as a constant: PPSA27836 calls it for every keycode from 0 to 0x8F to build a table of key names, upper-cases `a` to `z` itself and skips keycodes that fail or return 0, so that bit is ignored and the unshifted character is returned as for 0. Any other bit throws, and so does a keyboard type other than 0. The returned character for HID usage `0x64` has not been measured on a console; `\` is an unverified US-layout guess based on the Windows VK mapping, which does not establish the console result. ### Functional 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/libSceDepth.md b/docs/dev/debt/libSceDepth.md new file mode 100644 index 0000000000..9a67891d10 --- /dev/null +++ b/docs/dev/debt/libSceDepth.md @@ -0,0 +1,7 @@ +# libSceDepth technical debt + +### Unknown function info + +- [_sceDepthCalcHeteroAdjustParam, _sceDepthHeadCandidateTrackerGetInformation, _sceDepthHeadCandidateTrackerSetInformation, _sceDepthSetCameraFactoryCalibrationData, _sceDepthSetHeteroAdjustParam, _sceDepthSetHeteroMode](../../../core/libs/prx/libSceDepth/Export.cpp) (libSceDepth) - signatures and behavior are unverified; these parameterless declarations only resolve imports and throw through `NotImplemented_nid_no_patch`. + +- [libSceDepth pending exports](../../../core/libs/prx/libSceDepth/Export.cpp) - 32 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceDepth.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. diff --git a/docs/dev/debt/libSceDepth2.md b/docs/dev/debt/libSceDepth2.md new file mode 100644 index 0000000000..ee4e1f9b5c --- /dev/null +++ b/docs/dev/debt/libSceDepth2.md @@ -0,0 +1,5 @@ +# libSceDepth2 technical debt + +### Unknown function info + +- [libSceDepth2 pending exports](../../../core/libs/prx/libSceDepth2/Export.cpp) - 10 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceDepth2.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. diff --git a/docs/dev/debt/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/libSceEsvm.md b/docs/dev/debt/libSceEsvm.md new file mode 100644 index 0000000000..099c999a95 --- /dev/null +++ b/docs/dev/debt/libSceEsvm.md @@ -0,0 +1,5 @@ +# libSceEsvm technical debt + +### Unknown function info + +- [sceEsvmAppAudioSyncCreateContext, sceEsvmAppAudioSyncFlush and sceEsvmSyncVideo](../../../core/libs/prx/libSceEsvm/Export.cpp) (libSceEsvm) - signatures and behavior are unverified; these parameterless declarations only resolve imports and throw through `NotImplemented_nid_no_patch`. 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/libSceFont.md b/docs/dev/debt/libSceFont.md index 15c10ccaf8..5c3d5ad062 100644 --- a/docs/dev/debt/libSceFont.md +++ b/docs/dev/debt/libSceFont.md @@ -1,9 +1,5 @@ # libSceFont technical debt -### Silent stubs - -- [sceFontGlyphDefineAttribute](../../../core/libs/prx/libSceFont/src/Render.cpp) (libSceFont) checks the glyph and ignores the attribute and its value, whose meaning is unknown. PPSA01325 sets attribute 0x11 to 0 on a glyph from `sceFontGenerateCharGlyph` that it deletes unused, and draws the character with `sceFontRenderCharGlyphImageHorizontal` - ### Unknown function info - [sceFontSetScriptLanguage, sceFontGetScriptLanguage, sceFontSetTypographicDesign, sceFontGetTypographicDesign](../../../core/libs/prx/libSceFont/src/Style.cpp) (libSceFont) - signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.cs), not confirmed for the PS5. The handle keeps the language set for each script and the feature set for each typographic design and returns them, with 0 for one never set; the console default and its valid ranges are unknown, so any value is accepted. Neither setting changes shaping or rendering yet @@ -18,8 +14,11 @@ - [sceFontCharacterGetSyllableStringState](../../../core/libs/prx/libSceFont/src/Text.cpp) (libSceFont) - the signature `(textCharacter, int* syllableStringState)` follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs). Measured on PS4 firmware 11.00 `libSceFont.sprx` (not on a PS5): the state is 0 for Latin, CJK, Hangul, Arabic, Hebrew, Devanagari and the other scripts tried, and the library sets 1, 3 or 4 (never 2 on real text) for Thai while it creates the string; a null character or a null result returns `SCE_FONT_ERROR_INVALID_PARAMETER`, and a null character writes 0 to a non-null result. This returns 0 for every character outside the Thai block (U+0E00 to U+0E7F), as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/d2413fc2ebd91d1b7234197c2dd5bd48e1cb6a51/src/libs/libFont.cpp#L1805-L1815) does, and throws for Thai characters, whose cluster states are not modelled. SharpProspero's `SceFontSyllableString` names 0 NonDetection, 1 Starting, 2 Following, 3 Depending and 4 Connecting. PPSA06699 (Hot Shots Golf 2) calls it. - [sceFontCharactersRefersTextCodes, sceFontTextCodesStepNext and sceFontTextCodesStepBack](../../../core/libs/prx/libSceFont/src/Text.cpp) (libSceFont) - the signatures follow the [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json#L679) IDA type database and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Font/SceFont.Engine.cs#L395-L406), and the 64-byte `FontTextCodes` with the order pointer at 0 and the code at 8 follows SharpProspero's [SceFontTextCodes](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Font/SceFontStructs.cs#L216-L228). The traversal follows the PS4 11.00 `libSceFont.sprx` disassembly posted in [#4050](https://github.com/boykopovar/AnyPS5/pull/4050#issuecomment-6114504202), not a PS5 measurement: the cursor keeps the current, start and term characters at +0x28, +0x30 and +0x38; `Refers` never compares the character with the term and returns null for a clustered or synthetic character; `StepNext` returns null at the term after visiting it; `StepBack` returns null when the current character is the start; both steps skip synthetic characters. Assumed and not covered by the report, which says `StepNext` returns null only at the term: `StepNext` also returns null at the end of the chain (for example with a null term), and `StepBack` at the head of the chain. A null cursor or output returns null as in [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/d2413fc2ebd91d1b7234197c2dd5bd48e1cb6a51/src/libs/libFont.cpp#L1696-L1740); a null text character throws, as its console result is unknown +- [sceFontDefineAttribute, sceFontGetAttribute](../../../core/libs/prx/libSceFont/src/Style.cpp), [sceFontGlyphDefineAttribute, sceFontGlyphGetAttribute](../../../core/libs/prx/libSceFont/src/Render.cpp) (libSceFont) - the signatures `(handle, int attribute, int* oldAttribute)` and `(handle, int attribute, int* nowAttribute)`, and the same shape for a glyph, follow the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs) bindings, matching the IDA type database of [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/d2413fc2ebd91d1b7234197c2dd5bd48e1cb6a51/src/libs/libFont.cpp#L1249-L1264), not confirmed on a title. Every handle and every glyph keeps one attribute, an opaque int whose meaning and valid values are unknown: define stores the new value and returns the previous one, and get returns the stored value with its `attribute` argument unused, as in KytyPS5. An attribute never set is 0. The null checks and the invalid-object errors follow the other libSceFont functions and diverge from [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/d2413fc2ebd91d1b7234197c2dd5bd48e1cb6a51/src/libs/libFont.cpp#L1251-L1265), which returns `OK` for a null handle or glyph after writing 0 to the old-attribute pointer: every other handle and glyph entry point in this library reports the invalid object instead, and a title that passes one is already failing. shadPS4 leaves both exports unimplemented, so there is no second implementation to compare. PPSA01325 calls the glyph define with 0x11 on a glyph from `sceFontGenerateCharGlyph` that it deletes unused and draws with `sceFontRenderCharGlyphImageHorizontal`; the three bindings all read the third argument as an out pointer, not a value, so this call sets the glyph's attribute to 0x11. The attribute changes neither shaping nor rendering. + ### Functional -- [sceFontOpenFontSet](../../../core/libs/prx/libSceFont/src/SystemFont.cpp) (libSceFont) opens only the primary file of a system font set. Characters the console takes from the set's other files (Hangul, Thai and Arabic in Japanese and Chinese sets, Arabic in European sets) are reported as unsupported glyphs. The console file names are those of the PS4 firmware; the names of the PS5-only JG2 sets (`0x1A......`) are unknown, so they use the SSTJpPro files. When a console file is missing, an openly licensed substitute (Noto Sans, Noto Sans Mono, Noto Sans Thai, Noto Sans CJK) is loaded instead. It is not the system font: its metrics differ, so text width, line height and wrapping differ from the console. System font faces are loaded by FreeType in host memory, not in the font memory the title passes to the library: FreeType needs about 1 MiB per CJK face, which the title's font memory is not sized for. +- [sceFontOpenFontSet](../../../core/libs/prx/libSceFont/src/SystemFont.cpp) (libSceFont) - the other script files of a set are chosen from its type, not measured on a console: Vietnamese for low-byte bit 0x10, Thai for script bit 0x10, Japanese for 0x04, simplified Chinese for 0x80 (before Japanese with 0x08), Hangul for 0x20 and Arabic for low-byte bit 0x80, as the SDK set names (`..._JP_AR`, `..._GBCJK_TH`) listed by [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/font/font_internal.cpp) suggest; the file order and the Hangul file name (`SCEPS4Yoongd-Medium.otf`, `SCEPS4Yoongd-Bold.otf`) also come from shadPS4. A character the primary file lacks is taken from the first of them that has it; kerning between two files is 0, and `sceFontGetCharGlyphCode` and `sceFontGetFontGlyphsCount` answer for the primary file only +- [sceFontOpenFontSet](../../../core/libs/prx/libSceFont/src/SystemFont.cpp) (libSceFont) - the console file names are those of the PS4 firmware; the names of the PS5-only JG2 sets (`0x1A......`) are unknown, so they use the SSTJpPro files. When a console file is missing, an openly licensed substitute (Noto Sans, Noto Sans Mono, Noto Sans Thai, Noto Sans Arabic, Noto Sans CJK with its Korean face for Hangul) is loaded instead; its metrics differ, so text width, line height and wrapping differ from the console. System font faces are loaded by FreeType in host memory, not in the font memory the title passes to the library: FreeType needs about 1 MiB per CJK face, which the title's font memory is not sized for. - [sceFontGetCharGlyphMetrics, sceFontGetRenderCharGlyphMetrics](../../../core/libs/prx/libSceFont/src/Layout.cpp) (libSceFont) report the vertical metrics the way the [FreeType driver](../../../core/libs/prx/libSceFontFt/src/Driver.cpp) reports them for a rendered glyph, not confirmed on a title: FreeType's vertical advance, and for a face without vertical metrics a bearing of half the horizontal advance left of the horizontal bearing and of half the advance left over by the glyph height. - [Font writing lines](../../../core/libs/prx/libSceFont/src/WritingLine.cpp) (libSceFont) - `sceFontWritingLineWritesOrder`, `sceFontWritingLineRefersRenderStep`, `sceFontWritingLineGetRenderMetrics`, `sceFontWritingLineGetOrderingSpace` and `sceFontWritingLineClear` follow the PS4 11.00 `libSceFont.sprx` (firmware module, read from its disassembly and compared call for call on random sequences): runs are linked in visual order by embedding level, the space at each spaced join is summed for the line, steps walk the visual order from the left (form `0x12`) or from the right (form `0x11`) and the render metrics grow as steps are consumed. The PS5 library was not measured. Not from that module: form `0x10` is accepted and treated as left to right and a detail with id `0x0FD5` is accepted (the PS4 library rejects both); the memory argument of `sceFontCreateWritingLine` and the rest of the detail are not read (the PS4 library validates its library object there); an unknown writing form, another detail id, a null line pointer on creation, unknown and null handles (except in `sceFontWritingLineRefersRenderStep`, which returns null for a null line) and a null output of `sceFontWritingLineGetRenderMetrics` throw instead of returning the PS4 error codes, which are not confirmed for the PS5; `sceFontDestroyWritingLine` was not compared. Lines are allocated on the host, not in the font memory the title passes to the library: FreeType needs about 1 MiB per CJK face, which the title's font memory is not sized for. diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index c78d212af4..fa594755cb 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -2,6 +2,8 @@ ### Unknown function info +- [FT_Select_Charmap, FT_Set_Charmap, FT_Get_First_Char and FT_Get_Next_Char](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console signatures and errors are unverified; the LP64 guest adapter follows the bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c). Character iteration uses the low 32 bits of the input code, as that version does internally, and updates the guest's selected charmap pointer without refreshing unrelated glyph or outline data. Guest charmap fields are treated as read-only copies of the native records. + - [FT_Init_FreeType, FT_New_Face, FT_Set_Pixel_Sizes, FT_Get_Char_Index, FT_Load_Glyph, FT_Render_Glyph](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - the PS4 library is taken to be FreeType 2 built for LP64: the face, size, charmap, glyph slot, bitmap and outline layouts follow the public headers of the bundled FreeType 2.13.3 with a 64-bit `long`, which matches the offsets the OpenOrbis samples read (`face->glyph` at 0x98, `slot->advance` at 0x80, `slot->bitmap` at 0x98, `slot->bitmap_left` at 0xc0). Which FreeType version and options the console library is built with is unknown, so the behaviour and error codes are those of FreeType 2.13.3. The OpenOrbis font, keyboard, threading, system and SDL2 samples import these six functions ### Functional diff --git a/docs/dev/debt/libSceGameLiveStreaming.md b/docs/dev/debt/libSceGameLiveStreaming.md index b30576336c..24dfaa238f 100644 --- a/docs/dev/debt/libSceGameLiveStreaming.md +++ b/docs/dev/debt/libSceGameLiveStreaming.md @@ -2,4 +2,5 @@ ### Unknown function info +- [sceGameLiveStreamingEnableLiveStreaming](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - imported by PS4 titles, which do not load without it, and added without an implementation: declared without parameters, every call throws - [sceGameLiveStreamingGetCurrentStatus2](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - the status layout (user id, on-air flag, spectator, text and command message counts, broadcast resolution, 48 reserved bytes) is taken from shadPS4's `OrbisGameLiveStreamingStatus2`; the call reports that no stream is on air, with an invalid user id 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/libSceGnmDriverForNeoMode.md b/docs/dev/debt/libSceGnmDriverForNeoMode.md new file mode 100644 index 0000000000..726f106af0 --- /dev/null +++ b/docs/dev/debt/libSceGnmDriverForNeoMode.md @@ -0,0 +1,7 @@ +# libSceGnmDriverForNeoMode technical debt + +### Unknown function info + +- [libSceGnmDriverForNeoMode pending exports](../../../core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp) - 118 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceGnmDriverForNeoMode.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. + +- [libSceGnmDriverForNeoMode pending exports](../../../core/libs/prx/libSceGnmDriverForNeoMode/Export.cpp) - 45 named callable exports were identified in the PS5 3.20 export table and cross-checked against firmware 13.60. Signatures and behavior remain unverified; declarations use the unknown-signature convention and throw through `NotImplemented_nid_no_patch`. Evidence: [3.20 export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceGnmDriverForNeoMode.c). 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 a18b12954b..1dacc21ad2 100644 --- a/docs/dev/debt/libSceHmd2.md +++ b/docs/dev/debt/libSceHmd2.md @@ -2,18 +2,84 @@ ### Silent stubs -- [libSceHmd2](../../../core/libs/prx/libSceHmd2/Export.cpp) has no PS VR2 runtime: `sceHmd2Initialize` returns libSceHmd's `UNSUPPORTED_FEATURE` (0x81110016) for any param, so the other calls answer as before initialization and `sceHmd2Open` returns `NotInitialized` (0x8a720002). These results are restored from the behaviour before #7323 and are not measured on hardware: firmware 13.20's libSceHmd2, with the `/dev/hmd2_cmd` state measured on a retail PS5 with no headset connected (connected flag 0, status 2, user -1), initializes, returns a handle from `sceHmd2Open` and reports `NotDetected`, which the code past the initialization check models but no title reaches. PPSA01317 passes a pointer to 16 zeroed bytes, only tests the result for non-zero and then skips its VR set-up; with initialization succeeding it stopped on `sceHmd2ReprojectionQueryBufferSizeAlign` (#6350). That export, `sceHmd2ReprojectionQueryDisplayBufferSizeAlign` and `sceHmd2ReprojectionQuerySeeThroughBufferSizeAlign` stay unimplemented and throw pending console measurements of their sizes and alignments. The other reprojection and gaze exports and every [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) export throw +- [libSceHmd2](../../../core/libs/prx/libSceHmd2/Export.cpp) has no PS VR2 runtime: `sceHmd2Initialize` returns libSceHmd's `UNSUPPORTED_FEATURE` (0x81110016) for any param, so the other calls answer as before initialization and `sceHmd2Open` returns `NotInitialized` (0x8a720002). These results are restored from the behaviour before #7323 and are not measured on hardware: firmware 13.20's libSceHmd2, with the `/dev/hmd2_cmd` state measured on a retail PS5 with no headset connected (connected flag 0, status 2, user -1), initializes, returns a handle from `sceHmd2Open` and reports `NotDetected`, which the code past the initialization check models but no title reaches. PPSA01317 passes a pointer to 16 zeroed bytes, only tests the result for non-zero and then skips its VR set-up; with initialization succeeding it stopped on `sceHmd2ReprojectionQueryBufferSizeAlign` (#6350). That export, `sceHmd2ReprojectionQueryDisplayBufferSizeAlign` and `sceHmd2ReprojectionQuerySeeThroughBufferSizeAlign` stay unimplemented and throw pending console measurements of their sizes and alignments. The other reprojection and gaze exports throw, and [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) never initializes ### 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), [sceVrTracker2GetCoordinateSystem and sceVrTracker2GetPlayAreaBoundaryGeometry](../../../core/libs/prx/libSceVrTracker2/Export.cpp) (libSceVrTracker2) +- 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 - PPSA01317 imports declared without parameters, signatures unknown: the [libSceHmd2](../../../core/libs/prx/libSceHmd2/Export.cpp) exports that throw and the remaining [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) exports. Names from the aerolib NID list - PPSA15878 imports declared without parameters, signatures unknown: the [libSceHmd2](../../../core/libs/prx/libSceHmd2/Export.cpp) reprojection and gaze exports, `sceVrTracker2Finalize`, `sceVrTracker2GetPlayAreaOrientedBoundingBox` and `sceVrTracker2LocateCoordinateSystem` in [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp), and the `scePadVrController` trigger effect exports in [libScePad](../../../core/libs/prx/libScePad/Export.cpp). Names from the aerolib NID list -- [libSceHmd2 gaze and reprojection exports](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - `sceHmd2GazeGetCalibrationData`, `sceHmd2GazeGetCalibrationDataMaxSize`, `sceHmd2ReprojectionClearUserEventEnd`, `sceHmd2ReprojectionClearUserEventStart`, `sceHmd2ReprojectionGetCurrentOutput`, `sceHmd2ReprojectionSetUserEventEnd`, `sceHmd2ReprojectionSetUserEventStart`, `sceHmd2GazeActivateGazeTracking`, `sceHmd2GazeGetDeviceStatus`, `sceHmd2GazeGetUserCalibrationStatus`, `sceHmd2GazeInsertRawDataWithHandle`, `sceHmd2GazeSetEnabledEye`, `sceHmd2GazeSetUserCalibrationData`, `sceHmd2GazeSetUserCalibrationPoint`, `sceHmd2GazeStartUserCalibration`, `sceHmd2GazeStopUserCalibration`, `sceHmd2ReprojectionSeeThroughDraw`, `sceHmd2ReprojectionSeeThroughIC`, `sceHmd2ReprojectionVideoOutEq`, `sceHmd2ReprojectionWaitShader`, `sceHmd2SeeThroughCallbackGetPoseDataFunc`, `sceHmd2GazeObjectSelectionGetSelectedObject`, `sceHmd2GazeObjectSelectionInitialize`, `sceHmd2GazeObjectSelectionQueryMemory`, `sceHmd2GazeObjectSelectionTerminate`, `sceHmd2ReprojectionClear`, `sceHmd2ReprojectionSet`, `sceHmd2ReprojectionSetEventGpuEnd`, `sceHmd2ReprojectionSetEventGpuStart`, `sceHmd2RpeorjectionSetParam`, `sceHmd2SetFieldOfView`, `sceHmd2SetIpdOffset` are declared without parameters from the firmware export database (aerolib.csv); their guest signatures are unverified on hardware and every call throws +- [libSceHmd2 gaze and reprojection exports](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - `sceHmd2ReprojectionSeeThroughDraw`, `sceHmd2ReprojectionSeeThroughIC`, `sceHmd2ReprojectionVideoOutEq`, `sceHmd2ReprojectionWaitShader`, `sceHmd2SeeThroughCallbackGetPoseDataFunc`, `sceHmd2ReprojectionClear`, `sceHmd2ReprojectionSet`, `sceHmd2ReprojectionSetEventGpuEnd`, `sceHmd2ReprojectionSetEventGpuStart`, `sceHmd2RpeorjectionSetParam` are declared without parameters from the firmware export database (aerolib.csv); their guest signatures are unverified on hardware and every call throws +- [sceHmd2InternalSetPortStatus](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalLedDetectorStartBySlot](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalImageStartBySlot](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalSetPortConnection](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalSetCameraSeeThroughMode](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalSetFakeLensSeparationDistance](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalDfuSend](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalLedDetectorOpenBySlot](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalImageOpenBySlot](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalLedDetectorStopBySlot](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalSetDeviceConnection](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalLedDetectorCloseBySlot](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalSetSidetone](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalGetDeviceId](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalSetBrightness](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalImageStopBySlot](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalSetVibrationMode](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalGetVr2dData](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature +- [sceHmd2InternalImageCloseBySlot](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature ### 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 +- [Hmd2BeforeInitializeZero](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers 0 to these 45 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `vDgpdS2N9Rw` (sceHmd2InternalRegisterVibrationCallback), `Ri35gKTZpvE` (sceHmd2InternalUnregisterVibrationCallback), `9oyW8puiKeI`, `1HZNE6jhBnQ` (sceHmd2InternalPowerEventInitialize), `cdwAKBZLXjs`, `Yub1MF58jfY`, `JYXAuQFnPyw` (sceHmd2InternalDfuOpen), `VuPKqCu3wzw` (sceHmd2InternalDfuClose), `GSThagVyW5U` (sceHmd2InternalCrashReportOpen), `J4qE5N937MY` (sceHmd2InternalCrashReportClose), `9KnNj-dyASg`, `Qta2bkstbFk`, `5ZcObnDRBXE`, `AvPNn0qx4WQ`, `jp0apqNGgi8` (sceHmd2SetIpdOffset), `-kZT7X7OMg0` (sceHmd2InternalRelocalizerOpen), `1ewLtng-SVk` (sceHmd2InternalRelocalizerClose), `B0b8DeIUGes` (sceHmd2InternalTrackingOpen), `HclLg4Oj5yw` (sceHmd2InternalTrackingClose), `VbEvrQXsZyY` (sceHmd2InternalLedDetectorOpen), `bB6MKZi5UCQ` (sceHmd2InternalLedDetectorClose), `OXQ1EDf02XU` (sceHmd2InternalImageOpen), `IAxfMTvFm0M` (sceHmd2InternalImageClose), `qTJtAtaLeno` (sceHmd2InternalGenDataOpen), `j7Q16UX6l34` (sceHmd2InternalGenDataClose), `pmmNM06rLqU`, `zdQfyDK9Ods` (sceHmd2InternalSlamTraceOpen), `aPRwp9GnljM` (sceHmd2InternalSlamTraceClose), `MjY1gBSaed0` (sceHmd2InternalGazeOpen), `evAMjJYHqW4` (sceHmd2InternalGazeClose), `r93FLEZ3BBk` (sceHmd2InternalGazeStart), `IdYURdEdNFo` (sceHmd2InternalGazeStop), `CVcL2Cu2AEI` (sceHmd2GazeActivateGazeTracking), `NmKIfVxr2s0`, `KeDLkkp62Gg`, `+aadu6-ra+A`, `FgnTiwwGSzk`, `PWAUwOTHbbo` (sceHmd2InternalCreateSharedMemory), `WzaEApHnsP8` (sceHmd2InternalIsGameVr2d), `dXzQI8QVY7Y` (sceHmd2InternalIsMiniAppVr2d), `cCvFKGqRh1k` (sceHmd2InternalIsCommonDlgVr2d), `uj4-2VEAFPU` (sceHmd2InternalIsCommonDlgMiniAppVr2d), `sIaGStI-iLM` (sceHmd2InternalMapSharedMemory), `JzwpZOemDO8`, `IsC4R4bZFVU`. Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2BeforeInitializeNotInitialized](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720002` to these 9 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `sXaPEPY-fLA` (sceHmd2InternalOpen), `3F5inHhKzXg` (sceHmd2InternalGetDeviceInformation), `sJAiB-6p55U`, `E8hDydLjsSs`, `xEQkSb2WJ7Y`, `aaP4dnR4q-0`, `lEU0TeUoGwI`, `cFVyjHBpUUU`, `zROtb46jTj4`. Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2BeforeInitializeError0004](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720004` to these 15 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `kAMsjYHBbNQ` (sceHmd2InternalSetVolume), `5m73zmLpsQ0` (sceHmd2InternalResetViewPosition), `EPeH4xuQ2XQ` (sceHmd2InternalGetStatus), `TJ8lyRsjFY4` (sceHmd2InternalClearTrackerFts), `-XQVcLnyX68` (sceHmd2InternalSetSlamCommand), `giQwQoHhYYA` (sceHmd2InternalDfuStart), `zNc9IkgawMI`, `-tnG5UJPvAc` (sceHmd2GazeStartUserCalibration), `FPhFmG+jXuQ` (sceHmd2GazeStopUserCalibration), `CKlkvNngofQ`, `yL1CZaciFWE` (sceHmd2GazeSetUserCalibrationPoint), `qlzgyG3s0RY`, `kTIDrDLzMv0`, `9Gee0ef8a+U`, `ecZVjQdwmBI` (sceHmd2GazeSetEnabledEye). Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2BeforeInitializeParameterNull](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` to these 16 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `MNtiJTDOPAU`, `RRy9WmqZfgU` (sceHmd2InternalDecryptData), `uobR4m9lgwc` (sceHmd2InternalGetMirroringMode), `-XPk5KWBYdo` (sceHmd2InternalRelocalizerSetResultData), `J44idh9izZM`, `Ps0wAvYaa1E`, `kLnMR8oQ1MY`, `kMz4rbBxvjo`, `DIU1sFRRaiE` (sceHmd2GazeGetCalibrationData), `ZQvU4vDzZGQ` (sceHmd2GazeSetUserCalibrationData), `TJpyGGP9YEY`, `Dih+Odk+RC0`, `oaqdSJw+HVY`, `m35gtHuanXU`, `CTM7QPLJF9Q` (sceHmd2InternalGetFlipInfo), `+rExHeC9lP0` (sceHmd2GazeObjectSelectionInitialize). Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2BeforeInitializeParameterInvalid](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720009` to these 9 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `bBh5bCp9coo` (sceHmd2InternalSetPositionResetIcon), `G3PNDE2VjfY` (sceHmd2InternalSetPowerOff), `nScCT0OPU0o`, `6Q0Ng7OTN9o`, `-fJKh4SnFR0` (sceHmd2InternalSetBudgetMode), `jUA2s-l0npY` (sceHmd2InternalSetForceIcon), `+9XhyAIJVj4`, `eBGK-o6E4RQ` (sceHmd2InternalBindDeviceWithUserId), `MTuFeL9184E`. Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2BeforeInitializeReprojectionNotInitialized](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a72000b` to these 10 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `d3SEIYTP6wc`, `EVECbTvBezo`, `ciusPdJKjSU`, `DIwqTkcZiJo` (sceHmd2ReprojectionSetUserEventStart), `64ERZWVFBTE` (sceHmd2ReprojectionSetUserEventEnd), `z0+0gXUJWs8` (sceHmd2ReprojectionClearUserEventStart), `rjAd4nCY4DM` (sceHmd2ReprojectionClearUserEventEnd), `Acyg4vTdGm0` (sceHmd2ReprojectionGetCurrentOutput), `aGOWaEeg04I`, `9up+EPhw1No`. Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2BeforeInitializeError002C](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a72002c` to these 1 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `V1kLQmP-17U` (sceHmd2GazeObjectSelectionTerminate). Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2BeforeInitializeError1002](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a721002` to these 5 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `2LVD1EC5SYE`, `Bf87AL6zWxU`, `XKt7w1MODFY`, `Y5s22M-qUDA`, `JJKY-BtQJLg`. Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2BeforeInitializeError1003](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a721003` to these 1 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `9kj9vw8UShQ`. Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2BeforeInitializeMinusOne](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers -1 to these 12 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `EAkv8Mqt33E` (sceHmd2InternalRelocalizerStart), `Rv7bkmKvmz0` (sceHmd2InternalRelocalizerStop), `1oPZizn0SNA` (sceHmd2InternalTrackingStart), `TwCpE-nNrlo` (sceHmd2InternalTrackingStop), `jkqAeQfdtxw` (sceHmd2InternalLedDetectorStart), `+l0RQph81Z8` (sceHmd2InternalLedDetectorStop), `qsmAtGw1Ncc` (sceHmd2InternalImageStart), `uKhwYoMsIsA` (sceHmd2InternalImageStop), `WNdaigr94dI` (sceHmd2InternalGenDataStart), `P6S5ZVWf-H0` (sceHmd2InternalGenDataStop), `sun6NWE1sdg` (sceHmd2InternalSlamTraceStart), `--TFsJYXo04` (sceHmd2InternalSlamTraceStop). Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2BeforeInitializeValue0400](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x400` to these 1 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `BWqcwjCozzI`. Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2BeforeInitializeValue20000](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x20000` to these 1 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `PRz7TdBGUI0`. Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [libSceHmd2 pre-initialization sweep](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - the names in the entries above are the aerolib names of the NIDs where one exists; the error names `Error0004`, `Error002C`, `Error1002` and `Error1003` are placeholders for codes whose meaning is unknown, and `ReprojectionNotInitialized` is ours because every export answering `0x8a72000b` is a reprojection one. Exports that answered differently to null and to zeroed arguments, wrote output, returned a pointer-like value, answered `0x8a72000a` (which `sceHmd2Initialize` also returns for some applications) or crashed the probe are left as they were +- [Hmd2CheckFirstThenError0004](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 is null and `0x8a720004` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 20 exports: `9WeZr4kYfzk` (sceHmd2InternalGetFwVersion), `N+5h1hCOFAg` (sceHmd2InternalGetRequiredPUPVersion), `w2vEs2y5ZmE` (sceHmd2InternalGetOpticalParam), `L5gat1pe0y0` (sceHmd2InternalSetCameraSeeThroughParam), `4GnLHR9NJ1w`, `AHk7nGSzef0` (sceHmd2InternalGetTrackerFtsData), `ncjX9d3mc0M`, `l63QYAUuogc` (sceHmd2InternalGetSlamCommand), `mrcBySCdXSo` (sceHmd2InternalDfuGetStatus), `ieBra+td8sU` (sceHmd2InternalDfuGetUpdateStatus), `s+rzBnewgGs` (sceHmd2InternalDfuCheckAfterPvt), `Nfcm0GBdzZI` (sceHmd2InternalRelocalizerGetVersion), `Tc9rasWfrm4` (sceHmd2InternalSetHomeCalibration), `ckTbKTX4KZg`, `D7576PUbUjk` (sceHmd2InternalGazeReadStatus), `tTJ04SrBT8U`, `sDP7hZ-P6OI` (sceHmd2GazeGetDeviceStatus), `J+eVNbN9ndY` (sceHmd2GazeGetUserCalibrationStatus), `kozvHBNyxf0`, `638Tmg5y6U4`. Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckFirstThenZero](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 is null and 0 when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 8 exports: `8ETtFtp5zqY` (sceHmd2InternalGetVrMode), `CWPkBm3EXQA`, `R62liQQIAaA` (sceHmd2InternalGetFakeLensSeparationDistance), `-bcWLHsdzo0`, `JngJzgNbu8U` (sceHmd2SetFieldOfView), `xNBSz8xrGdk`, `yUVPfN-jRIU`, `BKPR3RZ5aO4`. Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckFirstThenError1002](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 is null and `0x8a721002` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 8 exports: `IG4y+drWAWw` (sceHmd2InternalRelocalizerReadData), `rhYUml9vppE` (sceHmd2InternalRelocalizerReadDataAsync), `6574xHS+jZk` (sceHmd2InternalRelocalizerReadData2), `mXzaY6xVoxg` (sceHmd2InternalTrackingReadData), `6af-HIZQQn4` (sceHmd2InternalTrackingReadData2), `6qWtdTHQsTk`, `pUm8t8xWhFg`, `A0XKI-WjxK8` (sceHmd2InternalImageReadData). Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckFirstThenMinusOne](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 is null and -1 when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 5 exports: `A9s2j8OioPQ`, `YIgtmrL+HLY`, `Ng8zEkGxDNs`, `EtslTnqO1xU` (sceHmd2InternalImageStart2), `hfrnRTryKQQ` (sceHmd2InternalImageSetConfig). Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckFirstTwoThenParameterInvalid](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 and argument 1 is null and `0x8a720009` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 3 exports: `PPX68fNToFM`, `LxPIfjHaY4c` (sceHmd2GazeObjectSelectionQueryMemory), `w8gBTTdg+po` (sceHmd2GazeObjectSelectionGetSelectedObject). Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckSecondThenError002C](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 1 is null and `0x8a72002c` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 3 exports: `tWJPX2ycRMM`, `5UMB9PJA70o`, `xQs62qCk1EI`. Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckFirstThenParameterInvalid](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 is null and `0x8a720009` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 2 exports: `MMc+bSlqkzk` (sceHmd2InternalSetVirtualDisplaySize), `NpZHgvSYgEA`. Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckAllSixThenZero](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 and argument 1 and argument 2 and argument 3 and argument 4 and argument 5 is null and 0 when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 2 exports: `z-serf9vsb0` (sceHmd2InternalPushVr2dData), `IRMUFu+uG5s`. Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckSecondThenParameterInvalid](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 1 is null and `0x8a720009` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 2 exports: `72U64E+0z6U`, `LCQzi5ILGrY` (sceHmd2GazeGetCalibrationDataMaxSize). Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckFirstThenError800d0203](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 is null and `0x800d0203` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 1 exports: `Im0qL0j9Ubg`. Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckFirstThenError1003](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 is null and `0x8a721003` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 1 exports: `ng6eI8uA0h4` (sceHmd2InternalImageGetMemoryLayout). Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckFirstThenError1010](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 is null and `0x8a721010` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 1 exports: `mPPGgjyJrCI`. Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckFirstTwoThenError1002](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 and argument 1 is null and `0x8a721002` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 1 exports: `ZupCJmV5BDI` (sceHmd2InternalImageGetFrameData). Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckFirstTwoThenError1003](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 and argument 1 is null and `0x8a721003` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 1 exports: `mAQZwOX1IHU` (sceHmd2InternalImageGetAverageLuminance). Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckFirstAndThirdThenZero](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 and argument 2 is null and 0 when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 1 exports: `oacDkQkinLo`. Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckFirstAndThirdThenError1003](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 and argument 2 is null and `0x8a721003` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 1 exports: `zn7PHGzzEyI`. Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckSecondThenInvalidHandle](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 1 is null and `0x8a720003` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 1 exports: `wEHILc7TP04` (sceHmd2GazeInsertRawDataWithHandle). Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckSecondThenError1002](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 1 is null and `0x8a721002` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 1 exports: `W-ZIlUXcrNA`. Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [libSceHmd2 null-argument sweep](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - `Error1010` and `Error800d0203` are placeholder names. Left as they were: the exports that write output (`omimgG5itSs`, `FBUEhH9tQ1A`, `6PeHcAaKYg8`, `yur+xPdWPAg`, `RnieDDp5ge4`, `jM3NEJA11yw`, `b0H-n-tUk0U`) and those whose answer with a single null argument is neither `0x8a720008` nor their all-present answer, which take a handle or slot number rather than a pointer first (`MXhloJ6Iw3w`, `19wrIIiqFgM`, `Z389vAm47Ys`, `cDcrvEG8j8k`, `tuqJO6O6cb4`, `fd4fu1cziaI`, `UxMg7iL4nRs`, `K1GELwlaaGc`, `RFn39VCHjGY`, `FRoSjceDv10`, `wiv8+7ogrq0`, `wQ44gBwBw44`) +- [Hmd2WriteStateTwo](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console writes the five bytes `02 00 00 00 00` at the start of its first argument and returns 0; a null first argument returns `0x8a720008`: `omimgG5itSs`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2WriteSize0x2000Align0x100](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console writes the 64-bit values `0x2000` through its first argument and `0x100` through its second and returns 0; a null first or second argument returns `0x8a720008`: `6PeHcAaKYg8`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2WriteFourConstants](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console writes the 16 bytes `e1 f0 74 3c d8 f0 74 3c 1f f1 74 3c d8 f0 74 3c` (four floats near 0.01495) at the start of its first argument and returns 0; a null first argument returns `0x8a720008`: `yur+xPdWPAg`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2WriteSixHalvesThenInvalidHandle](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console writes the 24 bytes `00 00 00 00 00 00 00 3f 00 00 00 3f 00 00 00 00 00 00 00 3f 00 00 00 3f` (floats 0, 0.5, 0.5, 0, 0.5, 0.5) at the start of its second argument and returns `0x8a720003` for every first argument tried (0, a pointer); a null second argument returns `0x8a720008`: `RnieDDp5ge4`, `jM3NEJA11yw`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2WriteTwelveHalvesThenInvalidHandle](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console writes the 48 bytes `00 00 00 00 00 00 00 3f 00 00 00 3f 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 3f 00 00 00 3f 00 00 00 00 00 00 00 00 00 00 00 00` (floats 0, 0.5, 0.5, 0, 0, 0, 0, 0.5, 0.5, 0, 0, 0) at the start of its second argument and returns `0x8a720003` for every first argument tried; a null second argument returns `0x8a720008`: `b0H-n-tUk0U`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2SlotUpTo3CheckSecondThirdThenZero](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console returns 0 for a first argument of 0 to 3 and `0x8a720009` for 4, 5, 8, 16, 255 and -1; a null second or third argument returns `0x8a720008` even with an invalid first argument. Its fourth to sixth arguments answered `0x8a720009` when null with an invalid first argument, which does not separate them from the first-argument check, so they are not looked at: `MXhloJ6Iw3w`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2SlotUpTo1ThenError1002](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console returns `0x8a721002` for a first argument of 0 or 1 and `0x8a720009` for 2 to 5, 8, 16, 255 and -1; its other arguments were only nulled with an invalid first argument, which answered `0x8a720009` either way, so they are not looked at: `19wrIIiqFgM`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2SlotUpTo1CheckSecondThenError1002](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console returns `0x8a721002` for a first argument of 0 or 1 and `0x8a720009` for 2 to 5, 8, 16, 255 and -1; a null second argument returns `0x8a720008` even with an invalid first argument: `Z389vAm47Ys`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2SlotUpTo1ThenMinusOne](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console returns -1 for a first argument of 0 or 1 and `0x8a720009` for 2 to 5, 8, 16, 255 and -1; the other arguments are not looked at, for the reason given above: `cDcrvEG8j8k`, `tuqJO6O6cb4` (sceHmd2InternalLedDetectorSetIrCameraConfigBySlot), `fd4fu1cziaI` (sceHmd2InternalLedDetectorSetPriorityConfigBySlot). Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2SlotUpTo2ThenMinusOne](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console returns -1 for a first argument of 0 to 2 and `0x8a720009` for 3 to 5, 8, 16, 255 and -1; the other arguments are not looked at: `UxMg7iL4nRs`, `K1GELwlaaGc` (sceHmd2InternalImageSetConfigBySlot). Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2SlotUpTo2ThenError1002](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console returns `0x8a721002` for a first argument of 0 to 2 and `0x8a720009` for 3 to 5, 8, 16, 255 and -1; the other arguments are not looked at: `RFn39VCHjGY`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2SlotUpTo2ThenError1003](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console returns `0x8a721003` for a first argument of 0 to 2 and `0x8a720009` for 3 to 5, 8, 16, 255 and -1; the other arguments are not looked at: `FRoSjceDv10`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2SlotUpTo2CheckSecondThenError002C](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console returns `0x8a72002c` for a first argument of 0 to 2 and `0x8a720009` for 3 to 5, 8, 16, 255 and -1; a null second argument returns `0x8a720008` even with an invalid first argument: `wQ44gBwBw44`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2IndexZeroCheckSecondThenZero](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console returns 0 for a first argument of 0 and `0x8a72100f` for 1 to 5, 8, 16, 255 and -1; a null second argument returns `0x8a720008` even with a nonzero first argument. Its third to sixth arguments answered `0x8a72100f` when null with a nonzero first argument, which does not separate them from the first-argument check, so they are not looked at: `wiv8+7ogrq0`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [FBUEhH9tQ1A](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - left unimplemented: before initialization it writes the 32-bit value 9 through its second argument and returns 0 when its first argument points at zeroed memory, but returns `0x8a720001` without writing when that memory holds 0xAA bytes, and its fourth to sixth arguments answer `0x8a720001` when null, so the first argument is an in-out structure whose fields are not known diff --git a/docs/dev/debt/libSceHttp.md b/docs/dev/debt/libSceHttp.md index cfcbd069b6..bc45f2c73b 100644 --- a/docs/dev/debt/libSceHttp.md +++ b/docs/dev/debt/libSceHttp.md @@ -8,20 +8,71 @@ - [sceHttpUriBuild](../../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) retains `//` for a non-opaque URI independently of the selected fields, following [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a8a70da595da2a96e0410309472ba4a7eb558144/src/core/libraries/network/http.cpp#L3551-L3558). Black Myth: Wukong builds its `Scheme` header with option 1 and expects `http://` or `https://`. Other partial field combinations follow the PS4 reference and remain unverified on PS5 hardware. - [sceHttpSetCookieEnabled](../../../core/libs/prx/libSceHttp/Export.cpp) - the two-argument signature and 0/1 enable values follow [VitaSDK](https://github.com/vitasdk/vita-headers/blob/master/include/psp2/net/http.h) and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp); PPSA01521 passes 0 to disable cookies. Disabling succeeds because the offline HTTP backend has no cookie engine; enabling throws. Invalid enable values use the same error as `sceHttpSetInflateGZIPEnabled`. Like the existing offline HTTP setters, the handle is not validated; requests still fail with the network error. Console behavior is unverified -- [sceHttpParseResponseHeader](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); parsing and error codes follow [shadPS4's reverse-engineered PS4 library](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp), not confirmed on a PS5 title or hardware. Folded values retain their line breaks and whitespace; an empty first line starts the value scan after its line feed. A consumed byte count above `INT_MAX` throws instead of wrapping. +- [sceHttpParseResponseHeader](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); the result, value bounds and error codes follow a payload process on a PS5 (firmware 13.60). Unmeasured there: a bare carriage return, and vertical tab or form feed around the value, which are skipped as blanks before it as before. A value end above `INT_MAX` throws instead of wrapping - [sceHttpParseStatusLine](../../../core/libs/prx/libSceHttp/src/Parse.cpp) (libSceHttp) - behaviour and error codes follow the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation of the PS4 library, not confirmed on a PS5 title; unlike it, no byte past `lineLen` is read - [sceHttpUriMerge](../../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) - signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); validation, size formula, absolute/relative split and path join follow the disassembly-like body in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp), not confirmed on a PS5 title or hardware. The base URL goes through our `sceHttpUriParse`, which removes dot segments from the base path, while the relative part is appended unswept; base query and fragment are dropped. The relative reference is only classified by its scheme and a leading `//`, and only references with `//` are validated; as in shadPS4, `//host/x` is returned unchanged instead of taking the base scheme, a reference with a scheme but no `//` (`mailto:x`) is joined onto the base directory, and `?q` and `#f` are appended to the base directory instead of the base path. -- [sceHttpUriUnescape](../../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) - signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); percent decoding, invalid sequences and error codes follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp), not confirmed on a PS5 title or hardware. Size includes the final NUL; malformed escapes and `+` remain unchanged, and decoding runs once. -- [sceHttpUriSweepPath](../../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) - behaviour follows the [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp) reimplementation of the PS4 library and its tests (including the PS4 SDK example), not confirmed on a PS5 title or hardware. It is not RFC 3986 5.2.4: a path that doesn't start with `/` is copied unchanged and a trailing `.` or `..` without a slash is kept. Unlike shadPS4, nothing at or past `srcSize - 1` is read, and `srcSize == 1` writes only the terminator +- [sceHttpUriUnescape](../../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) - signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); decoding and error codes follow a payload process on a PS5 (firmware 13.60). A buffer one byte short of the decoded size got the decoded bytes without a terminator and left `require` alone; a shorter buffer is assumed to get the first `prepare` bytes the same way +- [sceHttpUriSweepPath](../../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) - follows a payload process on a PS5 (firmware 13.60), which agrees with the [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp) reimplementation of the PS4 library: it is not RFC 3986 5.2.4, a path that doesn't start with `/` is copied unchanged and a trailing `.` or `..` is kept. Like the console, the byte at `srcSize - 1` is read as the `/` that ends a `..` segment, and `srcSize == 1` with a leading `/` writes `/`; whether it also ends a `.` segment was not measured, so a `.` there is still kept as before - [sceHttpSetResponseHeaderMaxSize, sceHttpRedirectCacheFlush, sceHttpsUnloadCert](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - signatures follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp) (PS4); no PS5 reference has them - [sceHttpSetRequestStatusCallback](../../../core/libs/prx/libSceHttp/Export.cpp) - the three-argument ABI follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp). Registrations and user data are retained and removed on request deletion; the offline backend never dispatches callbacks. Invalid ids return `0x80431100`, an uncited assumption, not a verified console error. Handle hierarchy, inheritance and deleted-handle validation remain incomplete; callback statuses and dispatch timing are unknown. - [sceHttpSetRedirectCallback, sceHttpSetCookieRecvCallback, sceHttpsSetSslVersion, sceHttpsGetSslError](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - signatures follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp) (PS4), which only logs and returns 0; a null `errNum` or `detail` in `sceHttpsGetSslError` is assumed to return `SCE_HTTP_ERROR_INVALID_VALUE` like `sceHttpGetLastErrno` - [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/libSceHttp2.md b/docs/dev/debt/libSceHttp2.md index 5327cfe772..2fa52cfbc5 100644 --- a/docs/dev/debt/libSceHttp2.md +++ b/docs/dev/debt/libSceHttp2.md @@ -7,12 +7,14 @@ ### Unknown function info - [sceHttp2WebSocketSetPingInterval](../../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2, NID `YfaXPnc8PJE`) - imported by `libdiscord_partner_sdk.prx` of PPSA23126 (issue 2648). The parameters, the unit of the interval and the console errors are unknown. Every call throws. -- [sceHttp2WebSocketSetPingTimeout](../../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2, NID `-MU9L6S3Fj4`) - imported by Onimusha: Way of the Sword (PPSA27836 01.000.001), whose loader stopped on the missing export; not called during its start-up. The parameters and the console errors are unknown. Every call throws. +- [sceHttp2WebSocketSetPingTimeout](../../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2, NID `-MU9L6S3Fj4`) - imported by PPSA27836 01.000.001, whose loader stopped on the missing export; not called during its start-up. The parameters and the console errors are unknown. Every call throws. - [sceHttp2DeleteCookieBox](../../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) - imported by PPSA35943's Unity player. The `(int cookieBoxId)` signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http2.cs#L215-L217). A box `sceHttp2CreateCookieBox` returned is discarded; an unknown or already deleted id returns the library's invalid-id error (0x817B1100), as [prosper](https://github.com/mattias800/prosper/blob/b1bec57186a0/prosper/src/hle/net/hle_http2.cpp#L512-L523) does; not measured on a console - [sceHttp2SetResolveRetry](../../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) - signature `(int id, int32_t retry)` from libSceHttp's `sceHttpSetResolveRetry`; the value is accepted and ignored like the other Http2 settings, since no request reaches the network - [sceHttp2WebSocketCreateRequest, sceHttp2WebSocketCloseAsync, sceHttp2WebSocketSendTextMessageAsync, sceHttp2WebSocketSendDataMessageAsync](../../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) - imported by PPSA16064 and not called during its start-up; unknown signatures - [libSceHttp2 cookie boxes](../../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) - `sceHttp2CreateCookieBox` is taken to take the library context id first, as in the [prosper](https://github.com/mattias800/prosper) reimplementation; further arguments are unknown and ignored. `sceHttp2SetRequestNoContentLength` is assumed to take only the request id. Cookies are never stored, so `sceHttp2CookieFlush` has nothing to discard + +- [libSceHttp2 pending exports](../../../core/libs/prx/libSceHttp2/Export.cpp) - 18 named callable exports were identified in the PS5 3.20 export table and cross-checked against firmware 13.60. Signatures and behavior remain unverified; declarations use the unknown-signature convention and throw through `NotImplemented_nid_no_patch`. Evidence: [3.20 export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceHttp2.c). ### Functional - [libSceHttp2 asynchronous requests](../../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) never reach the network: `sceHttp2SendRequestAsync` accepts the request, queues a completion whose result is the library's network error and triggers the user event named by the kqueue option (equeue at offset 0, event id at 8, user data at 16, layout inferred from PPSA17221); `sceHttp2WaitAsync` returns that completion. The completion's `event_type` value is unknown and left 0. PPSA17221 completes its request object only after `sceHttp2SendRequestAsync` returns, and its handler for a synchronous failure calls `shared_from_this` before that, so a send must not fail synchronously. PPSA15508 passes null kqueue and extra options at original VA 0xb63931, then calls `sceHttp2WaitAsync` with a 24-byte result and a 2,000,000 timeout, and checks the result at offset 8. A null kqueue option therefore retains the same offline completion for direct waiting without an event notification. The timeout is not used because the offline result is already complete. This path follows the original title call and caller control flow, not a console measurement. A supplied kqueue with a missing user event, an extra option, and a wait with no completed operation still throw diff --git a/docs/dev/debt/libSceHttpCache.md b/docs/dev/debt/libSceHttpCache.md new file mode 100644 index 0000000000..9ee1af9ece --- /dev/null +++ b/docs/dev/debt/libSceHttpCache.md @@ -0,0 +1,7 @@ +# 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 + +- The following pending declarations have unverified signatures and behavior; they are parameterless import-resolution declarations and throw through `NotImplemented_nid_no_patch`: [`sceHttpCacheClearAllUserTags`](../../../core/libs/prx/libSceHttpCache/Export.cpp), [`sceHttpCacheClearCacheInfo`](../../../core/libs/prx/libSceHttpCache/Export.cpp), [`sceHttpCacheDebugSetUrl`](../../../core/libs/prx/libSceHttpCache/Export.cpp), [`sceHttpCacheRegisterCacheInfoUrl`](../../../core/libs/prx/libSceHttpCache/Export.cpp), [`sceHttpCacheRemoveTag`](../../../core/libs/prx/libSceHttpCache/Export.cpp). diff --git a/docs/dev/debt/libSceIme.md b/docs/dev/debt/libSceIme.md index 76178899b3..465e86b085 100644 --- a/docs/dev/debt/libSceIme.md +++ b/docs/dev/debt/libSceIme.md @@ -2,15 +2,63 @@ ### 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 +- [sceImeFilterText](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the behaviour follows the disassembly of `libSceIme.sprx` from firmware 13.60 and is differential-tested against that function in an emulator, not measured on a console. [ImeFilterCodepages.hpp](../../../core/libs/prx/libSceIme/include/ImeFilterCodepages.hpp) holds the two codepage checks (helpers at 0x4850 and 0x48f0), rebuilt from Unicode's public [CP932](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP932.TXT) and [CP936](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP936.TXT) tables plus the Latin-1, CJK Extension A and a few other units the firmware also accepts (listed in the PR). The parameter layout beyond `type` is inferred: `mask` is the allowed-class bitmask, and `version` and `variant` derive it when it is 0. [sceImeCheckFilterText](../../../core/libs/prx/libSceIme/Export.cpp) throws, as its contract is unknown +- [sceImeOpen, sceImeUpdate, sceImeSetText, sceImeSetCaret, sceImeClose](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the event ids (Open 0, UpdateText 1, UpdateCaret 2, PressClose 4, PressEnter 5), the caret directions (left 1, right 2), the error codes and the text event payload follow the PS4 reimplementation in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/b72489d8c24e63fc56a29fe2c90120ab768a210d/src/core/libraries/ime/ime.cpp); none is measured on a PS5. Events are queued and delivered by `sceImeUpdate` only when it is passed the handler given to `sceImeOpen`, with that call's `arg`. A text event points `str` at a copy of the text kept in `work`, and `inputTextBuffer` holds the same text; both are terminated, `caret_index` is the caret after the edit and the one edit area has the edit position and the change in UTF-16 units (0 for Enter and Close). An edit sends one UpdateText; a caret move sends one UpdateCaret per UTF-16 unit, as shadPS4 does. Enter inserts a line break with the multiline option and sends PressEnter otherwise; Escape sends PressClose. Typed text is cut to `maxTextLength` without splitting a surrogate pair, line breaks are dropped without the multiline option, and the BasicLatin, URL, mail and number types do not filter characters. `sceImeOpen` checks, in shadPS4's order, the user, type, enter label, input method, option bits (the `sceImeGetPanelSize` mask), `maxTextLength` (1 to 2048), the text buffer, the position (below 1920x1080, or 3840x2160 with the over-2K option), the alignments, the extended priority and reserved bytes, `work` (non-null, 4-byte aligned), the handler, the reserved bytes and the initial text (no unpaired surrogate, no line break without the multiline option); shadPS4's SDK-dependent option, language and extended-option masks and its work-overlap check are not applied, and only the text copy is written to `work`. `sceImeSetText` keeps the caret unless the text became shorter, `sceImeSetCaret` rejects an index past the text with `0x80bc0030`, and neither sends an event. `sceImeSetTextGeometry` accepts any geometry while a panel is open, since no panel is drawn +- [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 + +- [libSceIme pending exports](../../../core/libs/prx/libSceIme/Export.cpp) - 2 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceIme.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. ### Functional -- [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..58f32f8d68 100644 --- a/docs/dev/debt/libSceImeDialog.md +++ b/docs/dev/debt/libSceImeDialog.md @@ -6,4 +6,8 @@ ### Unknown function info +- [sceImeDialogForTestFunction, sceImeDialogGetCurrentStarState, sceImeDialogInitInternal, sceImeDialogSetPanelPosition](../../../core/libs/prx/libSceImeDialog/Export.cpp) (libSceImeDialog) - their signatures follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/ime/ime_dialog.h#L34-L51), not measured on a console. They are declared solely to resolve known imports, `sceImeDialogForTestFunction` without parameters and the others with shadPS4's parameters; this is not an ABI claim, and every call throws. +- [sceImeDialogForceClose](../../../core/libs/prx/libSceImeDialog/Export.cpp) (libSceImeDialog) - not measured on a PS5: its `int (void)` signature and behaviour follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/ime/ime_dialog.cpp#L571-L601) (PS4). It closes a running or finished dialog, as `sceImeDialogTerm` does, without writing the typed text to the title's buffer, which shadPS4 updates while typing and this library only on Enter, and returns `0x80bc0107` with no dialog open, the `NotInUse` code of [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Dialog/ImeDialog.cs#L224-L225), while this library's `sceImeDialogTerm` returns `0x80bc0002` in the same state. Closes when: measured on a PS5 - [sceImeDialogInit, sceImeDialogGetStatus, sceImeDialogGetResult, sceImeDialogAbort, sceImeDialogTerm](../../../core/libs/prx/libSceImeDialog/Export.cpp) (libSceImeDialog) - the statuses (none 0, running 1, finished 2), the end statuses (ok 0, user canceled 1, aborted 2) and the errors of `sceImeDialogInit` for an open dialog (`0x80bc0001`), a null parameter (`0x80bc0031`), a maximum text length outside 1 to 2048 (`0x80bc0016`) and a null text buffer (`0x80bc0017`) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/b2d7ec7d31f9ad9426db93afe079db8679b3e1e0/src/core/libraries/ime/ime_dialog.cpp); none is measured on a PS5. A result, abort or term on a dialog that is not finished, running or open returns `0x80bc0002`, where shadPS4 returns `0x80bc0105` to `0x80bc0107`, and `sceImeDialogTerm` also closes a running dialog, which shadPS4 refuses. The text reaches the title's buffer only when Enter accepts it + +- The following pending declarations have unverified signatures and behavior; they are parameterless import-resolution declarations and throw through `NotImplemented_nid_no_patch`: [`sceImeDialogInitInternalWithSubInfo`](../../../core/libs/prx/libSceImeDialog/Export.cpp). diff --git a/docs/dev/debt/libSceIpmi.md b/docs/dev/debt/libSceIpmi.md new file mode 100644 index 0000000000..09625b0ec9 --- /dev/null +++ b/docs/dev/debt/libSceIpmi.md @@ -0,0 +1,5 @@ +# libSceIpmi technical debt + +### Unknown function info + +- [libSceIpmi pending exports](../../../core/libs/prx/libSceIpmi/Export.cpp) - 5 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceIpmi.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. diff --git a/docs/dev/debt/libSceJpegDec.md b/docs/dev/debt/libSceJpegDec.md index 342b2df171..e5b7252fa0 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. In `sceJpegDecDecode`, rejecting an output `image_mem_addr` not aligned to 4 bytes and rejecting an `alpha_value` above 0xFF follow libScePngDec validation and are not verified from a libSceJpegDec binary disassembly diff --git a/docs/dev/debt/libSceJson2.md b/docs/dev/debt/libSceJson2.md index c5ee284c4c..3a89284e01 100644 --- a/docs/dev/debt/libSceJson2.md +++ b/docs/dev/debt/libSceJson2.md @@ -5,12 +5,13 @@ - [sce::Json::Parser::parse with a read callback](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the callback's contract is taken from PPSA04540's: it returns 0 with the next character, and at the end of its data writes `'\0'` and returns -1. Characters are read until the callback returns non-zero or gives `'\0'`, and the text read is then parsed as by the `(text, size)` overload, so trailing characters after the value are rejected as there. Whether the console stops reading at the end of the top-level value, and what it returns when the callback fails before the end, are unknown - [sce::Json::Parser::parse](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - a null text pointer or zero size returns `JsonErrorParse` (0x80920101) without modifying the destination Value; whether the console distinguishes a null pointer from a syntax error with a separate parameter error code is unknown. A number that overflows a double (e.g. `1e309`) throws: whether the console rejects the document or stores an infinite real is unknown - [sce::Json::InitParameter2, InitParameterRtti and InitParameterRtti2](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - layouts inferred, not known. `InitParameter2` is taken as the 40-byte object of [SharpEmu](https://github.com/sharpemu/sharpemu/blob/1811cef94435db7655209fb39f65ddf258b93fd6/src/SharpEmu.Libs/Json/JsonExports.cs) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/eda6ba37ece127ab0396378404d99182e3e8a530/src/libs/libJson2.cpp): the allocator, its user data, the file buffer size, the 32-bit special float format type at 0x18 and 12 bytes both zero, and both constructors zero all of it. `InitParameterRtti2::setAllocatorRtti` writes the allocation parameters and user data where `setAllocator` writes the allocator, as both references do; whether the console also marks the object as using RTTI allocation is unknown. `InitParameterRtti`'s constructor writes the allocation parameters, user data and file buffer size to the first 24 bytes, an order inferred from its mangled arguments. The values of `SpecialFloatFormatType` and what they select are unknown, so `setSpecialFloatFormatType` stores any value and nothing reads it: `Initializer::initialize(InitParameter2)` ignores every field as `initialize(InitParameter)` does, serializing a non-finite real throws whatever the type, and whether the type lets the parser accept NaN or infinity is unknown. The setters return nothing; whether the console's return `this` is unknown -- [sce::Json::Object::Pair and sce::Json::Array::iterator members](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) imported by PPSA30601 - the signatures follow the mangled names, with `Array::iterator::operator++(int)` returning its copy through a result pointer before `this` as `Array::begin` does, since the iterator has a non-trivial destructor. `Pair(const String&, const Value&)` deep-copies both into the layout `Object` iteration already hands out (key at 0, value at 0x10) and zeroes the 8 bytes between them; what the console keeps there is unknown. The default-constructed iterator is singular +- [sce::Json::Object::Pair, sce::Json::Array::iterator and sce::Json::Object::iterator members](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the signatures follow the mangled names, with `Array::iterator::operator++(int)` and `Object::iterator::operator++(int)` returning their copy through a result pointer before `this` as `begin` does, since the iterators have non-trivial destructors. `Pair(const String&, const Value&)` and default `Pair()` deep-copy or initialize into the layout `Object` iteration already hands out (key at 0, value at 0x10) and zeroes the 8 bytes between them; what the console keeps there is unknown. Default-constructed iterators are singular - [sce::Json::Value::setTypeMismatchHandler, setElementAccessFailureHandler and setSpecialFloatHandler](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - members of `Value` that return an int, inferred from a caller that registers them on its root `Value` before parsing into it, with the handler and its context, and accepts 0 or 0x80848104 as success; they return 0. The PS Vita library's `Value::setNullAccessCallback` is such a member too ([vitasdk](https://github.com/vitasdk/vita-headers/blob/15605617cbbb23d266f88af28899fdb573e2355e/include/psp2/json.h#L638)). The handler signatures follow the mangled names. A handler applies to the `Value` it is set on and to the values that `Value` holds then, and `Parser::parse` gives the values it creates the handlers of the `Value` it parses into, since the title reads the errors its handlers record after accessing parsed members; copies, `push_back` and `Object::operator[]` do not carry them, and how the console finds the handler of an accessed value is unknown. A typed getter on a non-null value of another type calls the type mismatch handler with the requested type, the value, null and the context, and returns the value the handler returns if it has the requested type, else the getter's default; passing null as the third handler argument (the second `Value` pointer), and the order of the two `Value` pointers, remain unverified against the console. The element access failure handler is not called: a failed `operator[]` or `getValue` on a value carrying it throws, as the error code the console passes, which the title records as its error, is unknown; without one the access still reaches the null-access callback. The special float handler is not called: serializing a non-finite real throws with or without it, as the `FunctionType` values and the `bool*` output are unknown, and the title's handler returns a different replacement for each of its two types - [sce::Json::MemAllocator::notifyError](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the base class's virtual does nothing. A title's allocator that does not override it calls it through its own vtable; the library never calls the title's allocator, so it never reports an allocation failure. Whether the console's base implementation logs anything is unknown +- [sce::Json::String size, capacity, resize, compare and operator==(const String&), append(const char*, size_t), operator+=; Value::swap; Array::iterator and Object::iterator copy constructor, advance and operator==](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the signatures follow the mangled names and the behaviour is that of the matching `std::string` or container operation, with no cited source in KytyPS5, shadPS4, SharpProspero or disassembly. `append(text, length)` throws on a null text with a non-zero length; `operator+=` returns the String; `Value::swap` exchanges the node pointers; `advance` moves the iterator in place, returns it, and does not check the distance against the end. Whether the console returns the iterator from `advance` and how it bounds it is unverified +- [sce::Json::Value::operator bool](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - member of `Value` returning `bool` as a conversion operator, with no cited source in KytyPS5, shadPS4, SharpProspero, or disassembly: evaluates to `false` for `TypeNull` and boolean `false`, and `true` for boolean `true`. For all other value types it currently returns `true`; whether the console converts them, throws, or checks container emptiness is unverified +- [sce::Json::Object::insert and sce::Json::Object::erase](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - signatures and behaviors have no cited source in KytyPS5, shadPS4, SharpProspero, or disassembly: `insert` returns an iterator through a result pointer before `this` as `Object::begin` does, and takes a const Pair reference. If the key exists, it updates the existing value and returns an iterator to it; otherwise it appends a deep copy of the pair and returns an iterator to the new element; whether the console updates existing keys, appends duplicates, or ignores duplicate insertion is unverified. `erase` removes the item with matching key and returns 1 if found and removed, or 0 if not; whether the console returns size_t, bool, or an iterator is unverified ### Functional -- Imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [sce::Json::Value::operator bool](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) and [sceNpSessionSignalingGetGroupFromSessionId](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) -- PPSA30601 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [sce::Json::Object::insert](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2), whose return type, and so whether a result pointer precedes `this`, is unknown - [sce::Json::InitParameter2 base-object constructor](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - forwards to the complete-object constructor and uses its inferred layout and default field values; constructor equivalence and initialization bytes remain unverified on the console diff --git a/docs/dev/debt/libSceJxr.md b/docs/dev/debt/libSceJxr.md new file mode 100644 index 0000000000..20faabeefb --- /dev/null +++ b/docs/dev/debt/libSceJxr.md @@ -0,0 +1,5 @@ +# libSceJxr technical debt + +### Unknown function info + +- [libSceJxr pending exports](../../../core/libs/prx/libSceJxr/Export.cpp) - 4 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceJxr.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. diff --git a/docs/dev/debt/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 b2e3497c23..ff8c271f1f 100644 --- a/docs/dev/debt/libSceLibcInternal.md +++ b/docs/dev/debt/libSceLibcInternal.md @@ -10,10 +10,10 @@ - [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 - [posix_spawnattr_getflags, getpgroup, getschedparam, getschedpolicy, getsigdefault, getsigmask, setpgroup, setschedparam, setschedpolicy and setsigmask](../../../core/libs/prx/libSceLibcInternal/Spawn.cpp) (libSceLibcInternal) read and write the 0x30-byte attribute object at the offsets of the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x8D860 to 0x8D910: flags at 0, process group at 4, scheduling parameter at 8, policy at 0xC, default and masked signal sets at 0x10 and 0x20), which never fail there and return 0; the layout matched on a PS5 (firmware 13.60). Uninitialized attributes and null pointers, which the console dereferences, throw as the existing setters do - [sys_siglist and sys_nsig](../../../core/libs/prx/libSceLibcInternal/SignalList.cpp) (libSceLibcInternal) hold the 32 descriptions and the count of [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/siglist.c#L73-L107) `siglist.c` (`NSIG` 32, signal 0 to `SIGUSR2`); the console's table was not read -- [inet_aton, inet_addr, inet_ntoa and inet_ntoa_r](../../../core/libs/prx/libSceLibcInternal/Inet.cpp) (libSceLibcInternal) follow the FreeBSD code in the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x8ADE0 to 0x8B3D0) and matched it on a PS5 (firmware 13.60) for 20 `inet_aton` inputs; a null text, which the console dereferences, throws +- [inet_aton, inet_addr, inet_ntoa and inet_ntoa_r](../../../core/libs/prx/libSceLibcInternal/Inet.cpp) (libSceLibcInternal) follow the FreeBSD code in the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x8ADE0 to 0x8B3D0) and matched it on a PS5 (firmware 13.60) for 20 `inet_aton` inputs; a null text, which the console dereferences, throws. `inet_ntoa_r` formats through libkernel's `__inet_ntop`, so a null buffer with room for the text throws and a null buffer that is too small sets `ENOSPC`, as `__inet_ntop` does for a null destination; the console's `inet_ntoa_r` was not measured with a null buffer diff --git a/docs/dev/debt/libSceLibreSsl.md b/docs/dev/debt/libSceLibreSsl.md new file mode 100644 index 0000000000..b8330484a5 --- /dev/null +++ b/docs/dev/debt/libSceLibreSsl.md @@ -0,0 +1,5 @@ +# libSceLibreSsl technical debt + +### Unknown function info + +- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 399 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. diff --git a/docs/dev/debt/libSceLibreSsl3.md b/docs/dev/debt/libSceLibreSsl3.md new file mode 100644 index 0000000000..3fbd00e901 --- /dev/null +++ b/docs/dev/debt/libSceLibreSsl3.md @@ -0,0 +1,5 @@ +# libSceLibreSsl3 technical debt + +### Unknown function info + +- [libSceLibreSsl3 pending exports](../../../core/libs/prx/libSceLibreSsl3/Export.cpp) - 87 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl3.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. diff --git a/docs/dev/debt/libSceLoginDialog.md b/docs/dev/debt/libSceLoginDialog.md index 5e631eb2be..07a38ff414 100644 --- a/docs/dev/debt/libSceLoginDialog.md +++ b/docs/dev/debt/libSceLoginDialog.md @@ -3,3 +3,8 @@ ### Silent stubs - [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/libSceM4aacEnc.md b/docs/dev/debt/libSceM4aacEnc.md new file mode 100644 index 0000000000..fbb1f5292f --- /dev/null +++ b/docs/dev/debt/libSceM4aacEnc.md @@ -0,0 +1,6 @@ +# libSceM4aacEnc technical debt + +### Unknown function info + +- [libSceM4aacEnc](../../../core/libs/prx/libSceM4aacEnc/Export.cpp) (libSceM4aacEnc, sysmodule id `0x000000bc`) has no encoder, so all seventeen exports throw. PPSA03332 imports them and fails to load (`libSceM4aacEnc.prx: cannot open shared object file`) when the library is absent. The names are from the aerolib NID list: `sceM4aacEncClearContext`, `sceM4aacEncClearContextEx`, `sceM4aacEncCreateEncoder`, `sceM4aacEncCreateEncoderEx`, `sceM4aacEncDeleteEncoder`, `sceM4aacEncDeleteEncoderEx`, `sceM4aacEncEncode`, `sceM4aacEncEncodeEx`, `sceM4aacEncEncodeWithPriority`, `sceM4aacEncEncodeWithPriorityEx`, `sceM4aacEncFlush`, `sceM4aacEncFlushEx`, `sceM4aacEncFlushWithPriority`, `sceM4aacEncFlushWithPriorityEx`, `sceM4aacEncGetContext`, `sceM4aacEncInitialize` and `sceM4aacEncTerminate` +- [sceM4aacEnc*](../../../core/libs/prx/libSceM4aacEnc/Export.cpp) (libSceM4aacEnc) - five signatures follow [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json): `int32_t sceM4aacEncClearContext(int32_t m4aacEncHandle)`, `int32_t sceM4aacEncCreateEncoder(int32_t channels, int32_t samplingRate, int32_t bitRate, int32_t inputFormat, int32_t outputFormat, int32_t *pInternalError)`, `int32_t sceM4aacEncDeleteEncoder(int32_t m4aacEncHandle, int32_t *pInternalError)`, `int32_t sceM4aacEncEncode(int32_t m4aacEncHandle, void *pInput, uint32_t inputByte, void *pOutput, uint32_t *pOutputByte, int32_t *pInternalError)` and `int32_t sceM4aacEncFlush(int32_t m4aacEncHandle, void *pOutput, uint32_t *pOutputByte, int32_t *pInternalError)`. Orbital has no entry for the other twelve, which are declared without parameters, so their signatures are unknown 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..d1660178ce --- /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` returns `INVALID_HANDLE` (`0x80EE0004`), checked before `INVALID_ARG`, and `sceMoveOpen` returns `ALREADY_OPENED` (`0x80EE0007`) for a type and index that are already open; both codes are named in [shadPS4's move_error.h](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/move/move_error.h), which never returns them, so the results and their order are not measured. `sceMoveOpen` throws for a user other than the initial user, whose console result is 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 698407b44c..cdb38de437 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -2,21 +2,20 @@ ### Silent stubs -- [sceNetShowIfconfig, sceNetShowNetstat](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - return success without printing anything: the `()` signatures and the success return follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/net.cpp), which stubs both the same way. PPSA01530 fails to load without the `sceNetShowIfconfig` export (#3177); PPSA11386 imports `sceNetShowNetstat` but does not call it during start-up. What the console prints and where is unverified +- [sceNetShowIfconfig, sceNetShowNetstat, sceNetShowRoute](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - return success without printing anything: the `()` signatures and the success return follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/net.cpp), which stubs each the same way. PPSA01530 fails to load without the `sceNetShowIfconfig` export (#3177); PPSA11386 imports `sceNetShowNetstat` but does not call it during start-up. What the console prints and where is unverified ### 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 - [sceNetShowIfconfigWithMemory](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetShowPolicy](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [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 @@ -36,23 +35,23 @@ - [sceNetFreeAllRouteInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetArpInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetAppMemInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetGetDnsInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetGetDnsInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(info, int flags)` signature, the two IPv4 server slots in network byte order and the server count as the return value follow the PS4 [OpenOrbis musl resolver](https://github.com/OpenOrbis/musl/blob/2b0bb8ce2fa64ac255cb656785729d159a5f074c/src/network/resolvconf.c) and [OpenOrbis Net.h](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9dd4a92695538bdeb09fb056d06dd11725/include/orbis/Net.h); unverified on PS5 hardware. It reports the host DNS servers of the interface `sceNetCtlGetInfo` reports, never servers set through `sceNetSetDnsInfo`. Zeroing the unused slots and returning 0 without a host IPv4 interface are unverified. Nonzero flags and a null info throw because their console results are unknown - [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) - 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) - 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); 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 @@ -65,7 +64,7 @@ - [sceNetThreadCreate](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetThreadExit](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetThreadJoin](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetUsleep](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetUsleep](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(int microseconds)` signature and the `EINVAL` failure for a negative count follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net.cpp); other counts sleep through libkernel's `sceKernelUsleep`, a cancellation point, and return 0. Whether the console takes the count as signed, and what it returns, is unverified on hardware - [sceNetShowIfconfigForBuffer](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetShowRouteForBuffer](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetShowNetstatForBuffer](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature @@ -180,15 +179,19 @@ ### 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 -- [sceNetSetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) sets `SO_RCVTIMEO` and `SO_SNDTIMEO` as the host socket's timeouts. On Windows a receive or send on a stream socket that times out fails with `ETIMEDOUT`, where FreeBSD returns `EAGAIN` or the partial count: Winsock leaves the connection in an indeterminate state after a timeout and reports no partial count ([SOL_SOCKET options](https://learn.microsoft.com/en-us/windows/win32/winsock/sol-socket-socket-options)), so FreeBSD's behaviour needs the timeout waited for without the host option. A receive on a datagram socket that times out fails with `EAGAIN` +- [sceNetSetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) sets `SO_RCVTIMEO` and `SO_SNDTIMEO` as the host socket's timeouts. Windows sockets stay non-blocking and the library waits for readiness until the stored timeout. A stream receive that times out still reports `ETIMEDOUT` rather than FreeBSD's `EAGAIN`; a datagram receive reports `EAGAIN`. Blocking stream sends return the queued byte count on timeout, or `EAGAIN` when no bytes were queued. - [sceNetSetsockopt, sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) take `SO_SNDTIMEO` and `SO_RCVTIMEO` (0x1005, 0x1006) as a 16-byte `struct timeval`, as FreeBSD 11.4's [`sosetopt` and `sogetopt`](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/uipc_socket.c#L2657-L2690) do, and Sony's 0x1105 and 0x1106 as an `int` of microseconds. A [console log of ProsperoLight](https://github.com/blackbearreloaded/ProsperoLight/issues/49) (firmware 12.20) shows receives timing out every 100 ms after it gave 0x1006 a `{0, 100000}` timeval. The errors for a value shorter than a timeval (`EINVAL`) or out of range (`EDOM`), and `sceNetGetsockopt` cutting the timeval to the caller's length, follow FreeBSD's `sooptcopyin` and `sooptcopyout` and are not measured on a console - [sceNetSetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) stores `SO_ONESBCAST` (`0x10000`, [shadPS4's `ORBIS_NET_SO_ONESBCAST`](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net_types.h)) per socket and `sceNetGetsockopt` reads back the value that was set, as shadPS4 does; PPSA34501 sets it and crashes later when it fails with `EOPNOTSUPP`. Sends ignore it. FreeBSD 11.4's [ip(4)](https://man.freebsd.org/cgi/man.cgi?manpath=FreeBSD+11.4-RELEASE&query=ip&sektion=4) documents the like-named `IPPROTO_IP` option `IP_ONESBCAST`: a datagram sent to an interface's directed broadcast address, such as 192.168.2.255, leaves through that interface with its destination rewritten to 255.255.255.255, and only on a socket allowed to set `SO_BROADCAST`. Here host routing selects the outgoing interface and the destination is not rewritten. shadPS4 also leaves sends unchanged, and its [TODO](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net.cpp#L2467-L2470) reads the option the other way round: a send to 255.255.255.255 would be converted to the interface's broadcast address. Whether the console's `SOL_SOCKET` option behaves like `IP_ONESBCAST`, and what it reads back, is not verified on hardware. FreeBSD 11.4's `sogetopt()` reads several boolean socket options back as their flag bit (`so_options & optname`, `sys/kern/uipc_socket.c`), not as the value set; whether the console does so for `0x10000` is unknown - [sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) reads `SO_ERROR` from the host socket. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `sceNetConnect`; a second failure with the same code on the same socket without a `sceNetConnect` in between would be hidden. +- [sceNetSetsockopt, sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) set `TCP_NODELAY` (`IPPROTO_TCP`, 1) on the host socket of a stream socket and read it back as `TF_NODELAY` (4) or 0, as FreeBSD 11.4's [`tcp_default_ctloutput`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet/tcp_usrreq.c#L1626-L1650) does. Every other `IPPROTO_TCP` option (`TCP_MAXSEG`, `TCP_NOPUSH` and `TCP_KEEPIDLE` among them, which FreeBSD implements) fails with `ENOPROTOOPT`, and the `IPPROTO_TCP` level on a socket that is not a stream socket fails with `EINVAL`, as [`ip_ctloutput`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet/ip_output.c#L943-L944) answers it. A set on a connection that FreeBSD's [`tcp_ctloutput`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet/tcp_usrreq.c#L1498-L1502) refuses with `ECONNRESET` (refused, reset or in `TIME_WAIT`) succeeds here because the host accepts it. A null value with a nonzero length fails with `EINVAL`, where FreeBSD's `kern_setsockopt` returns `EFAULT`, and a get into less than an `int` fails with `EINVAL`, where `sooptcopyout` truncates. None of this was measured on a console. +- [sceNetGetSockInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) reports 0 for the name, pid, policy, priority, queue lengths, virtual ports, state, bandwidth, drops and waits, sets no status flag except `SELF` (0x1) and `NONBLOCK` (0x200000), and reports the host's buffer sizes where none was set. An IPv6 socket fails with `EAFNOSUPPORT` and is not counted or listed; shadPS4 lists it with zero addresses. `flags` bits other than 0x40 fail with `EINVAL`, where shadPS4 ignores `flags`; 0x40 adds sockets in TIME_WAIT ([SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs)), but closed sockets are not kept. Otherwise the `s` below 0 count and list, the return value and `EINVAL` for a null `info` or an `n` below 1 follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/06e813ff8c8e2a6d81b02cabfcc9543c3debf78e/src/core/libraries/net/net.cpp#L1498-L1522). Not measured on a console. - [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) - console ordering between competing readers and writers is unmeasured. The per-call behavior follows FreeBSD 11.4's `soreceive_generic` and `sosend_generic` (`sys/kern/uipc_socket.c`); `0x80` is FreeBSD's flag value (`sys/sys/socket.h`). On Windows native sockets stay non-blocking. `MSG_DONTWAIT` and guest `SO_NBIO` return one host transfer's result. Blocking stream sends queue the whole buffer with one deadline and return progress after a timeout or host error. SSL uses this path through the native owner, retaining the socket, its mode and timeouts. Readiness waits poll at most every 10 ms for local close, so idle blocked operations wake periodically. - [sceNetRecv, sceNetRecvfrom and sceNetRecvmsg](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) on a datagram socket with no local address wait on Windows until a bind, connect or send gives it one, checking the host socket every 10 ms. Winsock fails such a receive with `WSAEINVAL`, where FreeBSD 11.4's `soreceive_dgram` waits for a datagram (`sys/kern/uipc_socket.c`). A non-blocking socket fails with `EAGAIN` at once, and the receive timeout ends the wait with `EAGAIN`; when the address comes first, the call waits for a datagram only for what is left of the timeout. A `sceNetShutdown` of the receive side ends the wait and the receive returns 0, and a zero-length receive returns 0 without waiting, as `soreceive_dgram` does when the socket can't receive more or nothing was asked for. An abort or close ends the wait, and the host receive then fails as it did before - [sceNetResolverAbort and sceNetResolverStartNtoaMultipleRecords](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) follow shadPS4 ([net.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net.cpp), [net_resolver.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net_resolver.cpp)); the `(int rid, int flags)` signature of the abort also matches [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs). Unverified on hardware. Lookups run synchronously over the host resolver, which cannot be interrupted: an abort while a lookup runs on another thread returns at once, and that lookup fails with `EINTR` without writing its output only when the host call returns; the abort flags are then ignored, as shadPS4 does for a running lookup. With nothing running, abort flag 1 (NTOA preservation) or 2 (ATON preservation) makes the next `sceNetResolverStartNtoa*` or `sceNetResolverStartAton` call on that resolver fail with `EINTR`, reported by `sceNetResolverGetError`, after its arguments are checked. Unlike shadPS4, which ignores them, other abort flag bits throw, and `sceNetResolverStartNtoaMultipleRecords` throws for any flag, including async (1) and 0x10000 (IP address literals disabled), as `sceNetResolverStartNtoaMultipleRecordsEx` does - [sceNetResolverStartNtoa6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) resolves a host name to its first IPv6 address through the host resolver (`getaddrinfo` with `AF_INET6`) and writes the 16-byte address; the `(rid, hostname, addr, timeout, retry, flags)` signature follows `sceNetResolverStartNtoa`. A name with no IPv6 address fails with `RESOLVER_ENODNS` (`0x804101E1`) like a failed lookup; whether the console returns IPv4-mapped addresses instead is not verified. Timeout and retry are ignored as in `sceNetResolverStartNtoa`, and any nonzero flags throw because their meaning for this call is not known. Unverified on hardware. +- [sceNetInetPton](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) ends an IPv4 address at the first space, tab, LF, VT, FF or CR and ignores the rest (`"1.2.3.4 x"` converts), leaves the destination and `sceNetErrnoLoc` unchanged when a conversion fails, as PS4 firmware 10.71 does; not yet confirmed on PS5. Leading whitespace, any other trailing byte (`0x01`, `0xa0`) and IPv6 text with whitespace fail. diff --git a/docs/dev/debt/libSceNetCtl.md b/docs/dev/debt/libSceNetCtl.md index cd450e632c..f9fb92ef5c 100644 --- a/docs/dev/debt/libSceNetCtl.md +++ b/docs/dev/debt/libSceNetCtl.md @@ -6,7 +6,79 @@ ### Unknown function info +- [sceNetCtlGetInfoV6](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl, NID `Jy1EO5GdlcM`) - imported by PPSA08587. `(int code, SceNetCtlInfoV6* info)` with codes 1-5 (IP address, link-local address, default route, primary and secondary DNS) comes only from [SharpProspero](https://github.com/SvenGDK/SharpProspero), whose source is unknown; shadPS4 and fpPS4 only stub it. A null `info` answers `INVALID_ADDR`, a code outside 1-5 `INVALID_CODE`, and a host without an IPv6 address `NOT_CONNECTED`, as `sceNetCtlGetStateV6` reports it. The layout of each answer is unverified, so every code throws on a host with an IPv6 address - [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, NID `H5yARg37U5g`) - `(int eventType, int* errorCode)` comes only from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/NetCtl.cs#L246-L251), whose source is unknown; shadPS4 only stubs it. Callbacks registered with `sceNetCtlRegisterCallbackV6` receive no event, so event types 1-3 write 0 to `errorCode`; what a console writes before the first event is unmeasured. A null `errorCode` answers `INVALID_ADDR`, as `sceNetCtlGetResult` does, and other event types throw, as their error code on a console is unknown +- [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..7b134da79b --- /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, 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/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index 289e3d05e7..09f0ba458a 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -9,6 +9,7 @@ - [NGS2 render buffer capacity](../../../core/libs/prx/libSceNgs2.native/src/Render.cpp) is treated as available storage for one configured grain. PPSA01880 passes 303104 bytes for 256 stereo float frames (2048 bytes); rendering writes or clears only those frames and preserves the remaining capacity. Non-null silent outputs now undergo the same supported-format and minimum-size checks as audible outputs, with channel counts limited to 1 through 8. Unused null outputs remain ignored. The console's handling of surplus capacity and invalid silent output descriptors has not been measured; invalid descriptors throw rather than return guessed SDK errors. - [NGS2 ATRAC9 waveform loops](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) accepts one integer forward `smpl` loop. Inclusive RIFF endpoints and encoder-delay adjustment follow [vgmstream riff.c](https://github.com/vgmstream/vgmstream/blob/7dc938fa2f210943b37c7b6511852b516ef432ab/src/meta/riff.c). PPSA03747 supplies a 60-byte sampler chunk with one loop, `samplerData` 24, start 256, inclusive end 299224 and delay 256 for 298969 audible samples. The chunk size and loop count bound the records; `samplerData` is not interpreted. Output positions are assumed to use audible samples and an exclusive end. Blocks split into an optional intro, a repeated range and an optional tail; zero play count means infinite repeats, positive play count includes the first pass. ATRAC9 ranges include a preceding codec frame for decoder history. The output block layout, repeat-count mapping and error codes are unverified on hardware. Multiple, reverse, alternating and fractional loops throw. - [NGS2 submixer UserFx](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (control `0x20000004`) invokes the title's callback after input mixing on every playing grain, including silence. The 40-byte control and 48-byte process context follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/libraries/ngs2/ngs2_submixer.h). PPSA03747 installs a peak-meter callback; its code confirms the channel pointers, user data 0, channel count, grain size and sample-rate offsets and returns 0. Other user data words are preserved. Flags are assumed to be 1 after registration/restart and 0 thereafter, following UserFx2's first-process convention; this callback does not inspect them. Null handlers disable the effect, setup clears it, and nonzero callback results throw. These lifecycle and error semantics are unverified on console hardware. +- [NGS2 voice callback after setup](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - a sampler or mixer setup clears the voice callback with the rest of the voice state. PPSA18216 registers a block-end callback for a streamed music voice, later sets the same voice up again for one-shot ATRAC9 blocks with user data 0, and registers the callback again before it streams again; its callback dereferences the block's user data, so it must not run for the one-shot blocks. A setup made from inside the voice's own callback clears that callback as well, so a callback that sets its voice up again and queues the next block runs once instead of for every block; PPSA18216 never sets a voice up from inside its callback, so that ordering is unverified. Unverified on console hardware. - [NGS2 ATRAC9 streaming](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) keeps waveform duration and skip counts across compressed input blocks. The shared `APPEND` flag (2), called `ONLY_DATA` in the compressed-data reference, ignores an ATRAC9 continuation block's sample, skip and repeat counts, following [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/bb98f26433fa9e711c219aac1711fe8ecba5bf3a/src/libs/ngs2.cpp); PPSA03747 confirms a 262044-byte initial buffer declaring 11310532 samples, followed by 16384-byte buffers with flags 3. Incomplete superframes are retained until more input arrives. Input-block completion callbacks allow refills; the final callback of a logical waveform waits for its buffered PCM to drain, preserving the existing complete-block repeat behaviour. This buffering, callback timing, decoded-byte accounting and starvation silence are unverified on hardware. A closed ATRAC9 stream ending before its declared samples throws; fragmented ATRAC9 waveforms with repeats are unsupported. PCM streaming retains its existing append validation and early-close behaviour. - [NGS2 mastering voice gain](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - the param id 0x30000004 is taken from PPSA22520 and the layout from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `OrbisNgs2MasteringVoiceGainParam`; no reference gives the units or range. The levels are assumed to be linear factors applied when the mastering voice is mixed into the render buffer, so the voice's own samples stay unscaled; the LFE level applies to channel 3 of 6- and 8-channel voices, assuming the L R C LFE order of the AudioOut2 entries above, and the full-band level to the rest. Negative levels are accepted, and a new setup resets both levels to 1 - [NGS2 stereo mastering voice into a surround buffer](../../../core/libs/prx/libSceNgs2.native/src/Render.cpp) (libSceNgs2.native) - PPSA21402 renders a 2-channel mastering voice into a 6- or 8-channel render buffer, which the mixing stage rejected. The console's conversion is unknown: the left and right channels are assumed to go to the first two buffer channels (front left and right) and the remaining channels stay silent, with no centre, LFE or surround content. Any other channel count mismatch still throws @@ -21,8 +22,9 @@ - [NGS2 custom submixer rack](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4002) - structure layouts from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_custom.h`; module id 0x1f for UserFx2 and 0x1d for UserFx, voice parameter ids `0x40001f00 | module index` and `0x40001d00 | module index`, the process flags (1 on the first process call after setup, 2 after a parameter change, 0 otherwise) and the call order (setup per voice at rack creation, process after the inputs are mixed, cleanup at destruction) come only from [KytyPS5](https://github.com/KytyPS5/KytyPS5) and shadPS4. Only UserFx and UserFx2 modules on a single buffer are accepted: other module ids, more buffers, a module control handler, different input and output channel counts, setup flags and default rack options throw. Each module's state is a separate buffer, not the `state_offset` slice of the voice state, and `sceNgs2VoiceGetState` still throws for this rack. As in KytyPS5, UserFx2 process runs only on grains where an input produced samples, so an effect tail stops with the input and a pending flag 2 waits for the next audible grain - [NGS2 custom module ids](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native) - a custom rack with a module id other than 0x13 (sampler rack), 0x1d and 0x1f (submixer rack) throws. Trails in the Sky 2nd Chapter (PPSA35169) creates a rack with module id 0x20 (`NGS2: custom module id 0x20 is not implemented`, #4246); which module it is, and its structure, are unknown. - [NGS2 reverb rack](../../../core/libs/prx/libSceNgs2.native/src/Reverb.cpp) (libSceNgs2.native, rack 0x2001) - follows the PS4 firmware 11.00 `libSceNgs2.sprx`, read and checked against its output at 11.025 to 48 kHz, including parameter changes during playback (differences at float rounding level); not yet compared with the PS5 library. Structure layouts come from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_reverb.h` and the param ids 0x20010000 (setup) and 0x20010001 (I3DL2) from PPSA22520. Only the default `reverb_size` of 1 is handled, with its 14400-sample reflection buffer; other sizes and the library's half-rate mode throw. System sample rates below about 7.5 kHz, where an allpass delay would have no samples, throw. Setup only accepts equal input and output channel counts -- [sceNgs2ReportRegisterHandler, sceNgs2ReportUnregisterHandler](../../../core/libs/prx/libSceNgs2.native/src/Unimplemented.cpp) (libSceNgs2.native, NIDs `uBIN24Tv2MI`, `nPzb7Ly-VjE`) are throwing stubs. The signatures (report type, handler, user data, out handle) and (report handle) follow [SharpProspero](https://github.com/SvenGDK/SharpProspero) and [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_report.h`. No report is produced yet, so registering a handler cannot be honoured. PPSA04540 imports both and registers a CPU load handler (type 5) +- [sceNgs2ReportRegisterHandler, sceNgs2ReportUnregisterHandler](../../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native, NIDs `uBIN24Tv2MI`, `nPzb7Ly-VjE`) keep a registry of report handlers. Register rejects a null handler with an invalid argument and returns `INVALID_OUT_ADDRESS` for a null out handle, then stores the report type, handler and user data under a freshly allocated opaque handle; unregister removes and frees the matching record and throws for a handle that is not registered. The signatures (report type, handler, user data, out handle) and (report handle) follow [SharpProspero](https://github.com/SvenGDK/SharpProspero) and [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_report.h`, and the 24-byte `Ngs2ReportDataHeader` passed to a handler is its `OrbisNgs2ReportDataHeader`. No report is produced yet, so no registered handler is ever invoked and PPSA04540, which imports both and registers a CPU load handler (type 5), still receives none. - [sceNgs2SystemSetSampleRate, sceNgs2SystemSetUserData, sceNgs2SystemGetUserData, sceNgs2SystemLock, sceNgs2SystemUnlock](../../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native) - shadPS4 only checks the system handle (`INVALID_SYSTEM_HANDLE`) and SharpProspero names what each call does; neither gives the other limits. A sample rate of 0 and a null user data pointer throw, any other rate is accepted, and changing the rate while a sampler filter is enabled throws because the filter coefficients were computed for the old rate. `sceNgs2SystemLock` and `sceNgs2SystemUnlock` follow [prosper's notes](https://github.com/mattias800/prosper/blob/d61eb2c0ad2de879ef74df9c4aa050c512804a18/prosper/src/hle/audio/hle_audio.cpp#L4367-L4384) on the PS5 module: Lock takes a per-system lock and counts nested holds, Unlock releases one hold from any thread and returns 0 without a hold, and `sceNgs2SystemRender` only tries the lock: while another thread holds it, Render clears one grain of each output, leaves the render count and voices unchanged and returns 0. The other calls do not wait for the system lock. Peace Walker (PPSA18216) holds it while it spins on a flag that its audio thread clears only after `sceNgs2SystemRender` and eight `sceNgs2VoiceGetStateFlags` calls, so on the console neither can wait for it. Whether the console's voice and rack calls take the lock at all is not known. `sceNgs2RackLock` still holds the library's lock until `sceNgs2RackUnlock`, as before +- [sceNgs2RackSetUserData, sceNgs2RackGetUserData](../../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native) - the signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/Ngs2.cs#L418-L421) and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ngs2/ngs2.cpp#L86-L111), which only logs them; neither gives an error. A handle that is not a live rack answers `INVALID_RACK_HANDLE` before a null user data pointer is checked, and the null pointer throws, as in `sceNgs2SystemGetUserData`. A new rack starts with 0. Not measured on a console - [sceNgs2RackGetInfo](../../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native) - field order from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `OrbisNgs2RackInfo` with the 64-byte name of the other info structures (216 bytes); `type` is assumed to be the rack id divided by 0x1000, `min_grain_samples` 64 (the system minimum) and `state_flags` 1; `uid` comes from a per-process rack counter, `render_count` is the owning system's render count and `active_voice_count` counts voices that are not empty (paused and stopped voices included). The last process ratio and tick and `active_channel_work_count` are left zero - [sceNgs2VoiceQueryInfo](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - only info id 0x4001 is known: PPSA22520 passes an 8-byte buffer and branches on the first word being 1 or 2, so it is assumed to be the voice channel count (0 before the voice is set up); the second word is zeroed and other ids throw - [sceNgs2VoiceControl patch](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - a destination rack handle with input id 0 resolves to that rack's first voice; this matches the Dead Cells call in #4334, but selection for racks with multiple voices has not been checked against a console. Nonzero destination input ids still throw. `sceNgs2VoiceGetPortInfo` reports the resolved voice handle and zero delay. @@ -36,4 +38,6 @@ ### Functional +- [NGS2 rack lock and voice callbacks](../../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native) - `sceNgs2RackLock` gives the calling thread the rack, nested. Rack locks, renders and rack or system destroys exclude each other across threads and systems, including during a render's voice callbacks; a thread nests them, also from its own callbacks, and a callback that waits for another thread's rack lock or render never returns. Other NGS2 calls from other threads go ahead, and a block-end or repeat callback runs with the internal lock released. The console's behaviour for other threads' calls during a rack lock is not measured. PPSA36591 holds a rack lock on its main thread while it waits for one of its own locks, held by a thread in `sceNgs2VoiceGetStateFlags`; it froze while the rack lock held the internal lock. `sceNgs2RackUnlock` of a rack this thread has not locked returns `0x804A8202`; the console code is unknown. UserFx process handlers keep the internal lock. +- [NGS2 destroy from its own render](../../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native) - `sceNgs2RackDestroy` of a rack of the system being rendered, or `sceNgs2SystemDestroy` of that system, called from one of that render's voice callbacks throws, also when the callback renders another system whose own callback makes the call, because the render would go on using the destroyed rack. The console's behaviour is not measured. - [sceNgs2RackGetVoiceHandle voice index](../../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native) - an index at or beyond the rack's voice count returns `0x804A8305` and leaves the output handle untouched, after the null-output and rack-handle checks that keep their order and codes. The value is read from the PS5 `libSceNgs2.native.sprx` export `MwmHz8pAdAo` (firmware 10.01 at `0x11f10`, 12.70 at `0x12130`, and every module from 1.00 to 12.70 checked in [#5916](https://github.com/boykopovar/AnyPS5/pull/5916#issuecomment-6172832590)): an index at or past the rack's voice count returns it through the same error helper that answers `0x804A8010` for a null output, and the output handle is written only on success. The rack is checked before the output pointer, and an invalid rack returns `0x804A8202`. Heretic + Hexen (PPSA26525), DOOM + DOOM II (PPSA21444) and STAR WARS: Dark Forces Remaster (PPSA19728) create a one-voice reverb rack, take voice 0 and ask for voices 1 to 3 at start-up, log the failures and go on 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/libSceNpAuthAuthorizedAppDialog.md b/docs/dev/debt/libSceNpAuthAuthorizedAppDialog.md index 7411171423..e6b5fa19a6 100644 --- a/docs/dev/debt/libSceNpAuthAuthorizedAppDialog.md +++ b/docs/dev/debt/libSceNpAuthAuthorizedAppDialog.md @@ -3,3 +3,4 @@ ### Silent stubs - [libSceNpAuthAuthorizedAppDialog](../../../core/libs/prx/libSceNpAuthAuthorizedAppDialog/Export.cpp) - the authorized-app dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled, as there is no PSN sign-in to authorize an app against. The signatures follow the other common dialogs (`Open` takes a parameter block, `GetResult` writes a 32-bit result); Minecraft (PPSA17221) imports the seven functions and fails to load without them. +- [sceNpAuthAuthorizedAppDialogClose](../../../core/libs/prx/libSceNpAuthAuthorizedAppDialog/Export.cpp) - `sceNpAuthAuthorizedAppDialogClose` moves an initialized dialog straight to `COMMON_DIALOG_STATUS_FINISHED`, also when called before `Open`, following the Signin / NetCtlAp dialogs; MsgDialog / SaveDataDialog / ErrorDialog return NOT_RUNNING or INVALID_STATE for that case, and which one the real library does is unverified. diff --git a/docs/dev/debt/libSceNpCommerce.md b/docs/dev/debt/libSceNpCommerce.md index 7a9d0ec642..d9ac7be185 100644 --- a/docs/dev/debt/libSceNpCommerce.md +++ b/docs/dev/debt/libSceNpCommerce.md @@ -6,4 +6,5 @@ ### Unknown function info -- [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 +- [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. PPSA34501 calls it at Zombies > Online diff --git a/docs/dev/debt/libSceNpCommon.md b/docs/dev/debt/libSceNpCommon.md new file mode 100644 index 0000000000..2a8900021d --- /dev/null +++ b/docs/dev/debt/libSceNpCommon.md @@ -0,0 +1,156 @@ +# libSceNpCommon technical debt + +### Unknown function info + +The 150 declarations below use names mapped from the local `libSceNpCommon.sprx` export table. Each NID was verified against that table and recomputed from its name with `tools/nid_names.py`. The public [PS5 3.20 libSceNpCommon export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/main/libSceNpCommon.c) contains all 150 symbol names. No prototype or behavior is claimed. Each declaration uses the no-parameter unknown-signature convention solely for import resolution and throws if called. + +- [sceNpAllocateKernelMemoryNoAlignment](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `MWPOkqzYss0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpAllocateKernelMemoryWithAlignment](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `gMlY6eewr-c`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpArchInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `jGF+MaB4b-M`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpArchTerm](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `UskWpVWxSvg`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpAtomicCas32](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `+9+kKMY9YIw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpAtomicDec32](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Yohe0MMDfj0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpAtomicInc32](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `pfJgSA4jO3M`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpBase64Decoder](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `l67qBmMmKP4`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpBase64Encoder](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `pu39pU8UgCo`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpBase64GetDecodeSize](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `a5IfPlpchXI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpBase64UrlDecoder](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `moGcgMNTHvQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpBase64UrlEncoder](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `IeNj+OcWgU8`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpBase64UrlGetDecodeSize](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `7BjZKcN+oZ4`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpCalloutInitCtx](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `9+m5nRdJ-wQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpCalloutStartOnCtx](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `fClnlkZmA6k`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpCalloutStartOnCtx64](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `lpr66Gby8dQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpCalloutStopOnCtx](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `in19gH7G040`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpCalloutTermCtx](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `AqJ4xkWsV+I`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpCancelEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `kb2thTuS8t8`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpClearEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `9pLoHoPMxeg`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpCloseEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `+nmn+Z0nWDo`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpCloseSema](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `8hPzfjZzV88`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpCmpNpIdInOrder](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `TcwEFnakiSc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpCmpOnlineId](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `dj+O5aD2a0Q`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpCondDestroy](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `1a+iY5YUJcI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpCondInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `q2tsVO3lM4A`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpCondSignal](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `uMJFOA62mVU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpCondSignalAll](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `bsjWg59A7aE`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpCondSignalTo](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `bAHIOyNnx5Y`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpCondTimedwait](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `ss2xO9IJxKQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpCondWait](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `fZShld2PQ7w`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpCreateEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `6jFWpAfqAcc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpCreateSema](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `LHZtCT2W1Pw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpCreateThread](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `fhJ5uKzcn0w`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpDbgAssignDebugId](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `90pmGqDK4BI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpDbgDumpBinary](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Etq15-l9yko`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpDbgDumpText](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `ZaKa5x61hGA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpDeleteEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `sjnIeFCuTD0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpDeleteSema](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `xPrF2nGPBXQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpFreeKernelMemory](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `vjwlDmsGtME`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpGetNavSdkVersion](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `QmDEFikd3VA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpGetPlatformType](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `sXVQUIGmk2U`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpGetProcessId](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Z3mnqcGmf8E`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpGetRandom](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `pJlGhXEt5CU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpGetSdkVersion](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Pglk7zFj0DI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpGetSdkVersionUInt](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `ljqnF0hmLjo`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpGetSystemClockUsec](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `PVVsRmMkO1g`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpGlobalHeapGetAllocator](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `-gN6uE+zWng`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpGlobalHeapGetAllocatorEx](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `VUHUasztbUY`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpGlobalHeapGetAllocatorExPtr](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `P4YpPziLBd4`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpGlobalHeapGetAllocatorPtr](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `DI5n4aOdxmk`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpHeapDestroy](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `wVdn78HKc30`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpHeapGetAllocator](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `lvek8w7yqyE`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpHeapGetStat](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `2jdHoPpS+W0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpHeapInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `B+yGIX1+BTI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpHeapShowStat](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `evz0-93ucJc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpHexToInt](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Hvpr+otU4bo`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpInt32ToStr](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `5y0wMPQkaeU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpInt64ToStr](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `HoPC33siDD4`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpIntGetPlatformType](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `G6qytFoBJ-w`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpIntIsOnlineIdString](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `fY4XQoA20i8`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpIntIsValidOnlineId](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `hkeX9iuCwlI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpIntSetPlatformType](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `X6emt+LbSEI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpIntToHex](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `TWPY1x1Atys`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpIpc2ClientInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `kgDwlmy78k0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpIpc2ClientTerm](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `CI2p6Viee9w`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpJoinThread](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `EjMsfO3GCIA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpJsonParse](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `vJGDnNh4I0g`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpJsonParseBuf](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `RgfCYkjW7As`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpJsonParseBufInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `SnAdybtBK3o`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpJsonParseEx](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `p5ZkSMRR7AU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpJsonParseExInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `nhgjiwPUIzI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpJsonParseInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `teVnFAL6GNY`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpLwCondDestroy](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `zNb6IxegrCE`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpLwCondInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `++eqYdzB8Go`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpLwCondSignal](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Xkn6VoN-wuQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpLwCondSignalAll](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `FJ4DCt8VzVE`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpLwCondSignalTo](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Bwi+EP8VQ+g`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpLwCondWait](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `ExeLuE3EQCQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpLwMutexDestroy](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `4zxevggtYrQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpLwMutexInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `1CiXI-MyEKs`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpLwMutexLock](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `18j+qk6dRwk`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpLwMutexTryLock](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `hp0kVgu5Fxw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpLwMutexUnlock](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `CQG2oyx1-nM`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpMemoryHeapDestroy](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `dfXSH2Tsjkw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpMemoryHeapGetAllocator](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `FaMNvjMA6to`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpMemoryHeapGetAllocatorEx](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `xHAiSVEEjSI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpMemoryHeapInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `kZizwrFvWZY`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpMutexDestroy](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `lQ11BpMM4LU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpMutexInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `uEwag-0YZPc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpMutexLock](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `r9Bet+s6fKc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpMutexTryLock](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `DuslmoqQ+nk`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpMutexUnlock](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `oZyb9ktuCpA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpOpenEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `5DkyduAF2rs`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpOpenSema](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `-blITIdtUd0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpPanic](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `ZoXUrTiwKNw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpPollEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `9YmBJ8KF9eI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpPollSema](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `xmF0yIF4iXc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpRtcConvertToPosixTime](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `VMjIo2Z-aW0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpRtcFormatRFC3339](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `W0YWLVDndx0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpRtcParseRFC3339](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `LtkeQwMIEWY`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpServerErrorJsonGetErrorCode](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `0lZHbA-HRD0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpServerErrorJsonMultiGetErrorCode](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `cRabutqUG7c`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpServerErrorJsonParse](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `WSQxnAVLKgw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpServerErrorJsonParseInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `UbStlMKTBeU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpServerErrorJsonParseMultiInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `hbe+DdooIi4`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpSetEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `29ftOGIrUCo`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpSetPlatformType](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `m9JzZSoDVFY`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpSignalSema](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `-W28+9p1CKI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpStrBuildHex](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `i5TP5NLmkoQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpStrParseHex](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `DUHzVPNlugg`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpStrToInt32](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `fElyBSn-l24`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpStrToInt64](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `CwqYdG4TrjA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpStrToUInt32](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `uj86YxCYid0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpStrToUInt64](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Ted2YU9lv94`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpStrcpyToBuf](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `ivnnssCwjGI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpStrnParseHex](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `h1SWCcBdImo`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpStrncpyToBuf](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `PHrpHMSU8Cs`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpThreadGetId](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `yvaNTRiKXmo`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUInt32ToStr](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `rRN89jBArEM`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUInt64ToStr](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `QjNUYQbGoHA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUserGetUserIdList](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Gh74vNl06sg`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilBuildTitleId](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `N3tAHlBnowE`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilCanonicalizeNpIdForPs4](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `4mEAk-UKVNw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilCanonicalizeNpIdForPsp2](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `N3FB4r8JoRE`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilCmpAccountId](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `xPRHNaD3kTc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilGetDateSetAuto](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `owm52JoZ8uc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilGetDbgCommerce](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `1Gfhi+tZ9IE`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilGetEnv](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `kBON3bAtfGs`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilGetFakeDisplayNameMode](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `MUj0IV6XFGs`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilGetFakeRateLimit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `O86rgZ2azfg`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilGetIgnoreNpTitleId](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `FrxliFYAO8Y`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilGetNpDebug](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `GRvK1ZE+FEQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilGetNpLanguageCode](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `OFiFmfsADas`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilGetNpLanguageCode2](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `X9CqyP164Hc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilGetNpLanguageCode2Str](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Fxux7Ob+Ynk`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilGetNpLanguageCodeStr](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `RfiA17kV+xs`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilGetNpTestPatch](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `OA8f3KF9JsM`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilGetNthChar](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `KCk4OGu8+sc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilGetShareTitleCheck](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `fB5hE65pzbU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilGetSystemLanguage](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `SXUNKr9Zkv0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilGetTrcNotify](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `AjzLvR0g5Zs`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilGetWebApi2FakeRateLimit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `pmHBFJyju9E`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilGetWebApi2FakeRateLimitTarget](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `ZRxKp9vjcNc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilGetWebTraceSetting](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `4CqfNm3pisU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilHttpUrlEncode](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `ajoqGz0D9Dw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilJidToNpId](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `458yjI+OECI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilJsonEscape](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `EftEB4kmkSg`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilJsonGetOneChar](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `vj04qzp7uKY`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. +- [sceNpUtilJsonUnescape](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `4YJ5gYtRAAE`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. diff --git a/docs/dev/debt/libSceNpCppWebApi.md b/docs/dev/debt/libSceNpCppWebApi.md new file mode 100644 index 0000000000..be7f650ac6 --- /dev/null +++ b/docs/dev/debt/libSceNpCppWebApi.md @@ -0,0 +1,17 @@ +# libSceNpCppWebApi technical debt + +### Silent stubs + +- [sce::Np::CppWebApi::Common](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - the `finish` overrides of `TransactionBase<...>` (8 instantiations), `DownStreamTransaction<...>` (2 instantiations) and `Vector>::pushBack` return success without doing work; a failed request never produces a transaction to finish +- [sce::Np::CppWebApi::Leaderboards::V1](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - request-body and parameter bookkeeping succeeds without storing state: `GetRankingRequestBody::set{StartSerialRank,Group,Limit,Users,Offset}`, `RecordScoreRequestBody::set{Comment,SmallData,NeedsTmpRank,ComparedDateTime,PcId}`, `User::{setAccountId,setPcId}`, `BoardsApi::ParameterToGetBoardDefinition::{initialize,terminate}`, `RecordApi::ParameterToRecordLargeData::{initialize,terminate}`, `RecordApi::ParameterToRecordScore::{initialize,setxPsnAtomicOperation,terminate}`, `ViewApi::ParameterToGetLargeDataByObjectId::{initialize,terminate}`, `ViewApi::ParameterToGetRanking::{initialize,setgetRankingRequestBody,terminate}` +- [sce::Np::CppWebApi::SessionManager::V1](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - parameter bookkeeping succeeds without storing state: `PlayerSessionsApi::ParameterToCreatePlayerSessions::{ctor,dtor,initialize}`, `PlayerSessionsApi::ParameterToGetPlayerSessionInvitations::{ctor,dtor,initialize,setfields,setinvitationInvalidFilter}`, `PlayerSessionsApi::ParameterToJoinPlayerSessionAsPlayer::{ctor,dtor,initialize,terminate}`, `PlayerSessionsApi::ParameterToLeavePlayerSession::{ctor,dtor,initialize}`, `RequestPlayerSession::set{MaxSpectators,InvitableUserType,JoinableUserType,ExclusiveLeaderPrivileges}`, `RequestPlayerSessionPlayer::setPushContexts` +- [sce::Np::CppWebApi::TitleCloudStorage::V1](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - `DataApi::ParameterToDownloadData::{initialize,terminate}` succeed without storing state + +### Unknown function info + +- [sce::Np::CppWebApi](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - every request entry point (API calls, transaction `start`/`getResponse`/`readData`, the `*Factory::create`s) returns `SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN` offline. The constant's name and value follow the reference tables in shadPS4 `np_error.h` and fpPS4 `np_error.pas`, where `0x80552907` is `ORBIS_NP_WEBAPI_ERROR_NOT_SIGNED_IN` and `0x80552901` is `OUT_OF_MEMORY` (no `UNAVAILABLE` exists in the `0x80552901`-`0x8055291F` range); it is the code a signed-out console returns from these entry points. The earlier `0x80552901` value came from the module the previous implementation shipped, which used the same incorrect constant +- [sce::Np::CppWebApi::Common::IntrusivePtr](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - the 0x18-byte layout (object, deleter, `LibContext*`) and the offset of the 32-bit reference count in the pointee (0x00, 0x30 for `Vector`) come from the module bundled with one title (PPSA21564); other SDK versions are not checked. The destructor throws on the last reference of a pointer without a deleter: destroying the object needs its destructor and the `LibContext` allocator, which are not implemented +- [sce::Np::CppWebApi::Common::String](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - the 0x20-byte layout (32-bit reference count, buffer at 0x08, buffer size at 0x10, `LibContext*` at 0x18) comes from the module bundled with one title (PPSA21564); other SDK versions are not checked. `append` grows the buffer with the host allocator and the destructor frees it, instead of going through the `LibContext` allocator. `pushBack` into `Vector` stores a deep copy, so a destroyed temporary cannot invalidate the stored element; a title that byte-copies a `String` with a live buffer would still double-free, as nothing increments the reference count +- [sce::Np::CppWebApi::Common::Vector](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - imported by PPSA03671, whose dump ships no `libSceNpCppWebApi.prx`. A `Vector` handle is an `IntrusivePtr` over a shared body; bodies created by `pushBack` use a host-side layout with the reference count at 0x30, 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) - unimplemented stubs have their parameter lists, const qualifiers and return types aligned with the demangled symbols confirmed by NID matching in [#8143](https://github.com/boykopovar/AnyPS5/issues/8143). They cover response-body accessors, iterators, and uninstantiated transactions in Leaderboards::V1, SessionManager::V1, InGameCatalog::V5, TitleCloudStorage::V1, and Common templates: each mangled name gives the method and parameter types, but the runtime layout of response objects and errors are unverified on console, so each call continues to throw NotImplemented_nid_no_patch. No call site is recorded for them diff --git a/docs/dev/debt/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..622b40a2c2 100644 --- a/docs/dev/debt/libSceNpGameIntent.md +++ b/docs/dev/debt/libSceNpGameIntent.md @@ -3,8 +3,10 @@ ### Silent stubs - [sceNpGameIntentVshInitialize, sceNpGameIntentVshTerminate](../../../core/libs/prx/libSceNpGameIntent/Export.cpp) (libSceNpGameIntent) - VSH initialization and termination return 0 without performing shell operations, matching `sceNpGameIntentInitialize` and `sceNpGameIntentTerminate` +- [sceNpAppLauncherInitialize, sceNpAppLauncherTerminate](../../../core/libs/prx/libSceNpGameIntent/Export.cpp) (libSceNpGameIntent) - app-launcher lifecycle returns success without tracking launch state; `sceNpAppLauncherInitialize` accepts its init parameter without validation, following the silent-stub precedent of `sceNpGameIntentInitialize` — a title calls it early in boot, and returning success lets it continue ### Unknown function info +- [sceNpAppLauncherLaunchApp, sceNpGameIntentGetUdsNpComId, sceNpGameIntentLaunchApp, sceNpGameIntentLaunchApp2, sceNpGameIntentNotifyAppLaunched, sceNpSessionServiceGetJoinedPartySession, sceNpSessionServiceGetRunningGamePlayerSession and sceNpSessionServiceSendPlayerSessionInvitations](../../../core/libs/prx/libSceNpGameIntent/Export.cpp) (libSceNpGameIntent) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpGameIntent.c). Call sites in a title's executable give a partial prototype: `sceNpAppLauncherLaunchApp` takes `(user_id, uri struct, context)` and returns int — throws because no launcher-service semantics are derivable. The rest are declared without parameters solely to resolve known imports and make no ABI claim. - [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/libSceNpManager.md b/docs/dev/debt/libSceNpManager.md index e9f15ca299..9aedd80b56 100644 --- a/docs/dev/debt/libSceNpManager.md +++ b/docs/dev/debt/libSceNpManager.md @@ -2,6 +2,8 @@ ### Unknown function info +- [libSceNpManager pending exports](../../../core/libs/prx/libSceNpManager/Export.cpp) - 150 named callable exports are present in the local `libSceNpManager.sprx` export table and the public PS5 3.20 symbol list. Their signatures and behavior are unverified; each declaration uses the no-parameter unknown-signature convention and throws through `NotImplemented_nid_no_patch`. + - [sceNpGetSandboxInfo](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager, NID `ZVZ++68nNI8`) - the name hashes to the imported NID (found with nidhunt, confirmed with `tools/nid_names.py`). STAR WARS: Galactic Racer (PPSA28416) imports it; its OnlineSubsystemPS5 calls it with one pointer to a 0x50-byte stack buffer and reads the buffer as a string (discussions/3086). The returned string, the exact parameter struct and console errors are unknown, so every call throws without reading or writing caller memory. A working implementation belongs with that title's bring-up. - [sceNpUnknown_8weiQyIdI3A](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager, NID `8weiQyIdI3A`) - the name is not in aerolib. EA SPORTS FC 27 (PPSA34015) fails to load `libSceNpManager.prx` without it (`unresolved ELF import` / `GetLastError: 127`). Its only call is `mov edi, r14d` (a user id) and `lea rsi, [rbp-0x40]` after a 17-byte copy into that buffer (`mov edx, 0x11`); `test eax, eax` follows. The C name, the type of the 17-byte object and the console return are unknown, so every call throws without reading or writing caller memory. - [sceNpUnknown_WxLcN76q19o](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager, NID `WxLcN76q19o`) - the name is not in aerolib. PPSA34015 imports it from `libSceNpManager.prx` as a strong `JUMP_SLOT` with no call found in the eboot; the loader still requires the export. Arguments, result and console errors are unknown, so every call throws. @@ -13,3 +15,6 @@ - [NP reachability callbacks](../../../core/libs/prx/libSceNpManager/Export.cpp) retain one callback and its user data; duplicate registration and unregistering an absent callback return the errors used by [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/libraries/np/np_manager.cpp). No callback is delivered because the existing NP model remains signed out with reachability unavailable. - [sceNpCheckNpAvailability](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the `(req_id, online_id)` signature and the errors follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/np/np_manager.cpp) with no user signed in to PSN: a null online id is `SCE_NP_ERROR_INVALID_ARGUMENT`, and any other online id matches no user, `SCE_NP_ERROR_USER_NOT_FOUND`; the request id is not checked, as in the other NpManager requests - [NP state callbacks](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the registration rules are [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L857-L901)'s: `sceNpRegisterStateCallbackA` keeps up to 8 callbacks and returns the slot plus 1, a null callback returns `INVALID_ARGUMENT`, a callback already registered `CALLBACK_ALREADY_REGISTERED` and a ninth one `CALLBACK_MAX` (0x8055001D, [np_error.h](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_error.h#L38)); `sceNpUnregisterStateCallbackA` returns `INVALID_ARGUMENT` for an id outside 1..8 and `CALLBACK_NOT_REGISTERED` for an empty slot; the legacy [sceNpRegisterStateCallback](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L981-L1005) keeps one callback with the same errors. Each registration gets `SIGNED_OUT` for the initial user (0x10000000) once, from the next `sceNpCheckCallback` on the calling thread. For the A form this follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/network.cpp#L3959-L3974), whose [sceNpRegisterStateCallbackA](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/network.cpp#L4021-L4034) marks the registration pending. KytyPS5 never calls the legacy callback ([sceNpRegisterStateCallback](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/network.cpp#L4009-L4019) stores nothing); it is called as `(user, SIGNED_OUT, nullptr, userdata)`, as in shadPS4, which passes an NpId only for `SIGNED_IN` ([L823](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L823), [L938-L940](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L938-L940)). shadPS4 instead queues `SIGNED_OUT` from the user service login event and delivers it from `sceNpCheckCallback` ([DispatchPendingNpStateCallbacks](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L903-L962)); the reachability callback is not given an initial `UNAVAILABLE`, unlike that function ([L922-L961](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L922-L961)). A callback being delivered when another thread unregisters it is still called once. When and how often the console calls the state callbacks, and the errors, are not measured +- [sceNpRegisterGamePresenceCallback](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - accepts any callback, null included, and returns nothing; no game presence event is delivered because the NP model remains signed out. [Kyty](https://github.com/InoriRus/Kyty/blob/4733b7e1c91b10554a52007903d74dc76c39a230/source/emulator/src/Network.cpp#L1549-L1552) ignores the callback the same way, and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/8d50471b7dc141dd700b51276b8edd1f293bce26/src/core/libraries/np/np_manager.cpp#L1121-L1126) keeps the last one without calling it. Not measured on a PS5 + +- The following pending declarations have unverified signatures and behavior; they are parameterless import-resolution declarations and throw through `NotImplemented_nid_no_patch`: [`sceNpCheckCallbackForLib`](../../../core/libs/prx/libSceNpManager/Export.cpp). diff --git a/docs/dev/debt/libSceNpParty.md b/docs/dev/debt/libSceNpParty.md new file mode 100644 index 0000000000..45d68abf16 --- /dev/null +++ b/docs/dev/debt/libSceNpParty.md @@ -0,0 +1,5 @@ +# libSceNpParty technical debt + +### Unknown function info + +- [sceNpPartyGetState](../../../core/libs/prx/libSceNpParty/Export.cpp) (libSceNpParty) - the 16-bit state, its `NOT_IN_PARTY` value 2 and the `INVALID_ARGUMENT` (`0x80552504`) error follow shadPS4's [np_party.h](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/np/np_party.h) and [np_error.h](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/np/np_error.h) (PS4); shadPS4 does not check for a null state, and whether the console needs an initialized party library first is not known. Not measured on a PS5 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/libSceNpSnsFacebookDialog.md b/docs/dev/debt/libSceNpSnsFacebookDialog.md new file mode 100644 index 0000000000..3009c1d373 --- /dev/null +++ b/docs/dev/debt/libSceNpSnsFacebookDialog.md @@ -0,0 +1,5 @@ +# libSceNpSnsFacebookDialog technical debt + +### Unknown function info + +- The following pending declarations have unverified signatures and behavior; they are parameterless import-resolution declarations and throw through `NotImplemented_nid_no_patch`: [`sceNpSnsFacebookDialogClose`](../../../core/libs/prx/libSceNpSnsFacebookDialog/Export.cpp), [`sceNpSnsFacebookDialogGetStatus`](../../../core/libs/prx/libSceNpSnsFacebookDialog/Export.cpp). diff --git a/docs/dev/debt/libSceNpTrophy2.md b/docs/dev/debt/libSceNpTrophy2.md index 33aa1abb92..4576f12de6 100644 --- a/docs/dev/debt/libSceNpTrophy2.md +++ b/docs/dev/debt/libSceNpTrophy2.md @@ -10,6 +10,8 @@ - [sceNpTrophy2GetRewardIcon](../../../core/libs/prx/libSceNpTrophy2/src/Context.cpp) (libSceNpTrophy2) takes `(int context, int handle, int reward_id, void* buffer, size_t* size)`, from the call sites in the bundled `Media/Plugins/PSNCore.prx` of PPSA06888, the same shape as `sceNpTrophy2GetTrophyIcon`. The title calls it twice: first with a null buffer to read the size, then with a buffer of that size allocated with `malloc`. The meaning of the third argument is unverified, and the reward icon data is unknown, so every call throws + +- [libSceNpTrophy2 pending exports](../../../core/libs/prx/libSceNpTrophy2/Export.cpp) - 23 named callable exports were identified in the PS5 3.20 export table and cross-checked against firmware 13.60. Signatures and behavior remain unverified; declarations use the unknown-signature convention and throw through `NotImplemented_nid_no_patch`. Evidence: [3.20 export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpTrophy2.c). ### Functional - [Trophy queries](../../../core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp) report the title's [trophy data](../../../core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp) (`sce_sys/trophy2/trophyNN.ucp`) and the unlocks in the [trophy store](../../../core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp). Without the file, or when it is malformed, they report the old placeholders and `sceNpTrophy2GetTrophyInfoArray` lists only trophy 0. Icons are not read. Hidden trophies report their names and descriptions; whether the console blanks them before the unlock is not known. `progress_percentage` weighs gold 6, silver 2 and bronze 1 and leaves the platinum out, as KytyPS5 does (the ratio of the published PSN trophy points); the console's formula is not known. A trophy or group that is not in the file throws. A context that was not created or was destroyed reports placeholders and no unlocks and returns success; the console's errors for it are unknown. diff --git a/docs/dev/debt/libSceNpTus.md b/docs/dev/debt/libSceNpTus.md index 3ccc29383a..141e50a1cc 100644 --- a/docs/dev/debt/libSceNpTus.md +++ b/docs/dev/debt/libSceNpTus.md @@ -2,9 +2,115 @@ ### Unknown function info -- [SceNpTusVariableA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - the layout of the output array of `sceNpTusGetMultiSlotVariableAAsync` and of `outVariable` in `sceNpTusAddAndGetVariableAAsync` is not verified, so the output is never written and `variableArraySize` / `outVariableSize` are not checked. The signatures and the community error codes follow [shadPS4](https://github.com/shadps4-emu/shadPS4/tree/main/src/core/libraries/np/np_tus), which fpPS4 agrees with for the context and request calls; no console measurement. +- [TUS variable outputs](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - the layouts of the variable arrays of the `sceNpTusGetMultiSlotVariable*` calls and of `outVariable` in `sceNpTusAddAndGetVariableAAsync` are not verified, so the output is never written and `variableArraySize` / `outVariableSize` are not checked; shadPS4, and RPCS3 in its PS3 `sceNpTusGetMultiSlotVariable*` and `sceNpTusAddAndGetVariable*` calls, return `INVALID_ALIGNMENT` unless the size is `arrayLen` variables of the call's type, or one variable for `outVariable`. The signatures and the community error codes follow [shadPS4](https://github.com/shadps4-emu/shadPS4/tree/main/src/core/libraries/np/np_tus), which fpPS4 agrees with for the context and request calls; no console measurement. +- [Multi-slot variable calls](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - the argument checks of the multi-slot get, set and delete calls are not measured on a console; they follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/c1b26558352b863f80142256e4df38585aa7d727/src/core/libraries/np/np_tus/np_tus.cpp), where they differ between functions: `arrayLen < 1` is `INSUFFICIENT_ARGUMENT` in the NpId and virtual-user delete calls and in `sceNpTusGetMultiSlotVariableVUserAsync`, and `INVALID_ARGUMENT` in the others; a null NpId is accepted except in delete; an empty virtual user ID is `INVALID_ARGUMENT`. RPCS3's [PS3 module](https://github.com/RPCS3/rpcs3/blob/1a9dca3a3a84a18df866e3021918322bd295160a/rpcs3/Emu/Cell/Modules/sceNpTus.cpp) has only the NpId and virtual-user calls; it returns `INVALID_ARGUMENT` for an empty array in all of them, rejects a null target in get and set and does not check for an empty virtual user ID. +- [sceNpTusSetThreadParam](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTssCreateNpTitleCtx](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTssCreateNpTitleCtxA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusCreateTitleCtx](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusSetTimeout](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusChangeModeForOtherSaveDataOwners](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserVariable](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserVariableVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserVariableAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserVariableVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetFriendsVariable](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetFriendsVariableAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusAddAndGetVariable](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusAddAndGetVariableVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusAddAndGetVariableAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusAddAndGetVariableVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusTryAndSetVariable](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusTryAndSetVariableVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusTryAndSetVariableAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusTryAndSetVariableVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusDeleteMultiSlotData](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusDeleteMultiSlotDataVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusDeleteMultiSlotDataAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusDeleteMultiSlotDataVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusSetDataAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusSetDataVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetDataAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetDataVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotDataStatus](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotDataStatusVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotDataStatusAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotDataStatusVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserDataStatus](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserDataStatusVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserDataStatusAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserDataStatusVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetFriendsDataStatus](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetFriendsDataStatusAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTssGetSmallStorage](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTssGetSmallStorageAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTssGetStorage](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTssGetStorageAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTssGetDataAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusAddAndGetVariableA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusAddAndGetVariableAVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusAddAndGetVariableAVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusAddAndGetVariableForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusAddAndGetVariableForCrossSaveAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusAddAndGetVariableForCrossSaveVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusAddAndGetVariableForCrossSaveVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotDataStatusA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotDataStatusAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotDataStatusAVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotDataStatusAVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotDataStatusForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotDataStatusForCrossSaveAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotDataStatusForCrossSaveVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotDataStatusForCrossSaveVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetDataA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetDataAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetDataAVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetDataAVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetDataForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetDataForCrossSaveAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetDataForCrossSaveVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetDataForCrossSaveVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserVariableA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserVariableAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserVariableAVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserVariableAVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserVariableForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserVariableForCrossSaveAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserVariableForCrossSaveVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserVariableForCrossSaveVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserDataStatusA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserDataStatusAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserDataStatusAVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserDataStatusAVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserDataStatusForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserDataStatusForCrossSaveAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserDataStatusForCrossSaveVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserDataStatusForCrossSaveVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetFriendsVariableA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetFriendsVariableAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetFriendsVariableForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetFriendsVariableForCrossSaveAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetFriendsDataStatusA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetFriendsDataStatusAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetFriendsDataStatusForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetFriendsDataStatusForCrossSaveAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusSetDataA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusSetDataAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusSetDataAVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusSetDataAVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusDeleteMultiSlotDataA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusDeleteMultiSlotDataAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusTryAndSetVariableA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusTryAndSetVariableAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusTryAndSetVariableAVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusTryAndSetVariableAVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusTryAndSetVariableForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusTryAndSetVariableForCrossSaveAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusTryAndSetVariableForCrossSaveVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusTryAndSetVariableForCrossSaveVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusDeleteNpTitleCtx](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - the PS5 signature, error codes and child-request lifetime have not been measured on a console. Deletion invalidates the context and all its requests, following [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e7626e3c7de8d0f0fda6a91f620b42f6c762/src/core/libraries/np/np_tus/np_tus.cpp#L158-L180). ### Functional - [sceNpTusCreateNpTitleCtxA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - creates a title context without checking sign-in, up to 32 contexts (`TOO_MANY_OBJECTS` beyond); shadPS4 returns the `sceNpGetNpId` error instead. Requests are limited to 256, and the limits are not measured on a console. -- [sceNpTusPollAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - there is no PSN, so the four `*Async` calls that validate their arguments start a request that finishes at once with `SCE_NP_ERROR_SIGNED_OUT` (`ABORTED` after `sceNpTusAbortRequest`), reported by the poll result. Poll returns 1 until an operation has started. The slot, option and array-length checks follow shadPS4 and are not measured on a console. +- [sceNpTusPollAsync, sceNpTusWaitAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - there is no PSN, so every implemented `*Async` call whose arguments pass the checks starts a request that finishes at once with `SCE_NP_ERROR_SIGNED_OUT`, which poll and wait report and the blocking calls return; shadPS4 returns `SIGNED_OUT` from the `*Async` call itself when no shadNet client is signed in, and RPCS3 returns `INVALID_ONLINE_ID` while offline, marked there as probable behaviour. An abort stays set, so every later operation on the request, blocking ones included, finishes with `ABORTED`; in shadPS4 an abort only ends the operation in progress. Until an operation has started, poll returns 1 and wait returns `INVALID_ARGUMENT` as in shadPS4; RPCS3 [blocks until a result is set](https://github.com/RPCS3/rpcs3/blob/1a9dca3a3a84a18df866e3021918322bd295160a/rpcs3/Emu/NP/np_contexts.cpp#L40-L52). diff --git a/docs/dev/debt/libSceNpWebApi2.md b/docs/dev/debt/libSceNpWebApi2.md index c805a3decc..14eab1d91c 100644 --- a/docs/dev/debt/libSceNpWebApi2.md +++ b/docs/dev/debt/libSceNpWebApi2.md @@ -6,6 +6,10 @@ ### 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. +- [sceNpWebApi2InitializeForPresence, sceNpWebApi2IntInitialize and sceNpWebApi2IntInitialize2](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - create a library context like `sceNpWebApi2Initialize`, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/8d50471b7dc141dd700b51276b8edd1f293bce26/src/core/libraries/np/np_web_api2/np_web_api2.cpp#L174-L222) does on the PS4: the argument structures (32 and 40 bytes) and the `struct_size` check that returns `SCE_NP_WEBAPI2_ERROR_INVALID_ARGUMENT` follow its [declarations](https://github.com/shadps4-emu/shadPS4/blob/8d50471b7dc141dd700b51276b8edd1f293bce26/src/core/libraries/np/np_web_api2/np_web_api2.h#L15-L31). The context type shadPS4 records (2 for the internal initializers, 3 for presence), the context name and the push configuration group are not kept, and the 0x8000 context limit is not applied. Not measured on a PS5 +- [sceNpWebApi2CreateMultipartRequest and sceNpWebApi2SendMultipartRequest](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - 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 +- [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 the initializer that created the context, 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/libScePad.md b/docs/dev/debt/libScePad.md index 16bd404604..c52607d8ee 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -2,7 +2,7 @@ ### Unknown function info -- [scePadUnknown_n3kSX62fgNo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad, NID `n3kSX62fgNo`) - firmware 13.60 returned `0x80920005` before `scePadInit` and `0x809200ff` after it in the #3177 probe; the tested call read or wrote no argument pointers. PPSA15645 calls the import after `scePadOpen`, but its return and side effects in that state are unknown. The NID name and signature are unknown, so the statuses are not named by presumed error meanings +- [scePadUnknown_n3kSX62fgNo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad, NID `n3kSX62fgNo`) - the name is unknown, and the 0 returned after `scePadInit` is inferred, not measured. In the OS 10.01 system module the function takes no arguments, returns `0x80920005` before `scePadInit`, and otherwise sends a per-process value of 1 to ShellCore over Mbus, returning `0x809200ff` if that call fails and 0 if it succeeds, with or without an open handle. The #3177 probe measured `0x809200ff` in a process that is not a game; a game process is assumed to get 0. The ShellCore setting it changes is unknown and not modelled - [scePadSetAngularVelocityDeadbandState](../../../core/libs/prx/libScePad/src/PadState.cpp) (libScePad) - with the deadband on, each angular velocity axis whose magnitude is below 0.017453292 rad/s (1 degree per second) reads as 0, before the orientation is integrated; it starts off. Threshold, per-axis test and default follow [duaLib](https://github.com/WujekFoliarz/duaLib/blob/03bad1bea2a36561b520846776f2a07ced6773e0/src/source/duaLib.cpp#L44), which reimplements the PC `libScePad.dll`; the console's threshold is not measured, and the deadband is kept per library rather than per handle since one pad handle exists - [scePadOpen and scePadOpenExt](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - special ports (type 2) return `DEVICE_NOT_CONNECTED` (0x80920007) because the host has no special controller, and `scePadGetHandle` has no handle for them; whether the console opens a handle and reports the device as disconnected later is unknown. scePadOpenExt handles only special ports. PPSA01317 passes a 16-byte param that names the device by vendor and product id (`b7 0e 04 0e ...` for 0x0eb7:0x0e04) and skips the user on any negative result. Other port types throw - [scePadVrControllerReadState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad, NID `iA-DdobUen8`) - the name hashes to the imported NID; the `(int handle, void* state)` signature follows [PS5VR](https://github.com/theghostonline/PS5VR/blob/0e408909ce7a39891a2e06eee3438d07a09e11e3/app/src/app_vr.c#L326-L369) (PPSA99177), which passes a PS VR2 Sense controller handle from `scePadOpen` type 3 or 4 and a 0x68-byte state it reads as buttons at 0, stick bytes at 4 and a connected byte at 0x40. The title calls it only after `sceHmd2Initialize` succeeds, which it does not here, so every call throws; the state layout is otherwise unknown @@ -10,7 +10,7 @@ - [scePadSetVibrationMode](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - modes 1 to 3 and 5 are accepted and stored. On a PS5 (firmware 13.60) the call returns `0x80920005` before `scePadInit`, then rejects a mode of 0 or below with `INVALID_ARG` before it checks the handle, and returns `INVALID_HANDLE` for a bad handle with any higher mode; whether an open handle accepts modes above 3 was not measured, since the payload could not open a pad; 5 is accepted because PPSA25286 passes it on its open handle at start-up; the meanings of modes 3 and 5 are unknown (PPSA21480 calls it with 3 right after `scePadOpen` and aborts on failure; [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Pad/Pad.cs) names only 1 and 2). The stored mode has no other effect - [scePadIsRemoteController](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the name hashes to NID `fCWdlnmB1Ks`; the signature `(int handle, bool* remote)` comes from PPSA16064, which zeroes a byte, passes its address after `scePadGetControllerInformation` and ignores the result. The error codes follow the other pad calls. Controllers are local devices, so the answer is always false - [scePadSetAngularVelocityBiasCorrectionState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - inferred signature `(int handle, bool enabled)` follows `scePadSetTiltCorrectionState` and `scePadSetAngularVelocityDeadbandState`; an invalid handle returns `PAD_ERROR_INVALID_HANDLE`, disabling is a no-op, and enabling throws because bias correction is not modelled -- [scePadUnknown_zBsgF0q8DIM](../../../core/libs/prx/libScePad/Export.cpp) (libScePad, NID `zBsgF0q8DIM`) - PPSA03671 imports it and fails to load without it. The aerolib list has no name for the NID, and none of the pad names in SharpProspero, shadPS4 or OpenOrbis, nor the trigger effect, motion sensor, VR controller, HD and haptic names tried, hashes to it. It is declared without parameters and throws. The title's executable has an import stub for it but no call, jump or address reference to that stub, so the argument count and whether the result is used are unknown +- [scePadGetAccessibilityControllerState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the value a console reports for a DualSense is not measured; the emulated pad, which is not an Access controller, reports 0. The name hashes to NID `zBsgF0q8DIM`. In the OS 10.01 system module the `(int handle, uint32_t* state)` function returns `0x80920005` before `scePadInit`, then `INVALID_ARG` for a NULL `state` and `INVALID_HANDLE` for a bad handle, and otherwise asks the pad driver for a per-device value of the device behind the handle: 0 when no device is behind it, 1 when the driver has no answer, else the driver's value. What the values mean is unknown - [scePadSetTriggerEffectExt](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadLastLotusButtonData](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadReadStateExt](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature @@ -160,4 +160,7 @@ - [scePadControllerUpdateResetController](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadControllerUpdateGetReconnectStatus](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadControllerUpdateTerm](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature -- [scePadUnknown_n3kSX62fgNo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad, NID `n3kSX62fgNo`) - PPSA01530 and PPSA13356 import it and fail to load without it. The real name is unknown; it is declared without parameters and throws + +### Functional + +- [Touchpad resolution](../../../core/libs/prx/libScePad/include/PadState.hpp) (libScePad) - the library reports a DualSense to every title, so the controller information reports its touchpad, 1920 by 1080 at 44.86 pixels per cm, and the host controller's fingers and the keyboard touch contacts are placed in that range. [duaLib](https://github.com/WujekFoliarz/duaLib/blob/0b39f92261e2ff0e77eb0dac00943be6970e0dbf/src/source/duaLib.cpp#L847-L849) reports these values for a DualSense and 1920 by 943 at 44 for a DualShock 4, and the Linux kernel's [hid-playstation](https://github.com/torvalds/linux/blob/cd4f494ba535e848e2d299707d363de84e61a518/drivers/hid/hid-playstation.c#L177-L178) holds 1920 by 1080 against 1920 by 942 for the DualShock 4. A DualShock 4 on the host reports through the DualSense range too, so its last touchpad rows are never reached. The console's values are not measured 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/libScePlayGoDialog.md b/docs/dev/debt/libScePlayGoDialog.md new file mode 100644 index 0000000000..7e06e1309c --- /dev/null +++ b/docs/dev/debt/libScePlayGoDialog.md @@ -0,0 +1,5 @@ +# libScePlayGoDialog technical debt + +### Unknown function info + +- [scePlayGoDialogGetResult](../../../core/libs/prx/libScePlayGoDialog/Export.cpp) (libScePlayGoDialog) - checks that the dialog has reached STATUS_FINISHED and returns 0x80B80005 (COMMON_DIALOG_ERROR_NOT_FINISHED) when called before the dialog completes, following the other common dialog modules in the tree (MsgDialog, SigninDialog, PlayerSelectionDialog, SaveDataDialog). shadPS4's scePlayGoDialogGetResult is a dummy that always returns OK and provides no evidence for either value; the exact return code and intermediate state check have not been verified against a real hardware trace or title measurement. diff --git a/docs/dev/debt/libScePlayerSelectionDialog.md b/docs/dev/debt/libScePlayerSelectionDialog.md index c8920961b9..c4936f8740 100644 --- a/docs/dev/debt/libScePlayerSelectionDialog.md +++ b/docs/dev/debt/libScePlayerSelectionDialog.md @@ -3,3 +3,7 @@ ### Silent stubs - [libScePlayerSelectionDialog](../../../core/libs/prx/libScePlayerSelectionDialog/Export.cpp) - the player-selection dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled. `GetResult` writes only the first `int32` (`USER_CANCELED`); the rest of `ScePlayerSelectionDialogResult` (player list pointer and length) is not filled. EA SPORTS FC 27 (PPSA34015) fails to load `libScePlayerSelectionDialog.prx` without `Initialize`, `Open`, `Close`, `GetStatus`, `UpdateStatus` and `GetResult` (`unresolved ELF import` / `GetLastError: 127`) + +### Unknown function info + +- [scePlayerSelectionDialogParamInitialize](../../../core/libs/prx/libScePlayerSelectionDialog/Export.cpp) (libScePlayerSelectionDialog) - returns `COMMON_DIALOG_ERROR_ARG_NULL` on a null parameter and throws `NotImplemented_nid_no_patch` for non-null pointers because `ScePlayerSelectionDialogParam` struct layout and default fields remain unverified. diff --git a/docs/dev/debt/libScePngDec.md b/docs/dev/debt/libScePngDec.md new file mode 100644 index 0000000000..2eca53adb5 --- /dev/null +++ b/docs/dev/debt/libScePngDec.md @@ -0,0 +1,6 @@ +# libScePngDec technical debt + +### 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/libSceRazorCpu.md b/docs/dev/debt/libSceRazorCpu.md index 30115eb282..3a4d1b15d4 100644 --- a/docs/dev/debt/libSceRazorCpu.md +++ b/docs/dev/debt/libSceRazorCpu.md @@ -4,6 +4,8 @@ - [sceRazorCpuPushMarker](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signature (label, color, flags) from [SharpProspero](https://github.com/SvenGDK/SharpProspero), the same as sceRazorCpuPushMarkerStatic's except that the label is copied; it does nothing without a capture and returns 0 - [sceRazorCpuSync](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - the `int(void)` signature and its meaning, an unnamed frame-boundary event, come from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Debug/RazorCpu.cs#L119-L122). Without a capture there is no event to record, so it does nothing and returns 0, like the other `sceRazorCpu*` markers here and the [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/libraries/razor_cpu/razor_cpu.cpp#L143-L146) stub. PRAGMATA (PPSA02530) continues on 0; the console's result with no capture attached is unverified +- [sceRazorCpuDisableFiberUserMarkers, sceRazorCpuBeginLogicalFileAccess and sceRazorCpuEndLogicalFileAccess](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signatures and meanings from SharpProspero ([fiber markers](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Debug/RazorCpu.cs#L73-L79), [logical file access](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Debug/RazorCpu.cs#L189-L209)): the first makes later markers carry the thread instead of the fiber, the other two attribute the thread's file reads and writes to a named logical file. Both only change what a capture records, so without one they do nothing, and the two file-access calls return 0 for any arguments. Whether the console checks the operation (0 read, 1 write) or an unmatched end without a capture is unverified +- [sceRazorCpuInitDataTags, sceRazorCpuShutdownDataTags, sceRazorCpuTagBuffer, sceRazorCpuTagArray, sceRazorCpuResizeTaggedBuffer and sceRazorCpuUnTagBuffer](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signatures from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Debug/RazorCpu.cs#L130-L187), which documents them as the data sampler's buffer-tag table and the tags in it. Without a capture there is no data sampler, so they neither read the caller's storage nor keep the tags, and return 0 for any arguments; the console's errors for a tag before `sceRazorCpuInitDataTags`, an unknown buffer or a full table are unverified ### Unknown function info @@ -11,3 +13,4 @@ - [sceRazorCpuPushMarkerStatic, sceRazorCpuPopMarker](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signatures from PPSA26344's calls; (name, color, flags) assumed for the push; both do nothing without a capture - [sceRazorCpuPlotValue, sceRazorCpuWriteBookmark](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signatures (series, float value) and (label, optional description) from [SharpProspero](https://github.com/SvenGDK/SharpProspero), matching PPSA06320's calls; both do nothing without a capture and return 0 - [sceRazorCpuNamedSync](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - NID `XCuZoBSVFG8` is named by [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/razor_cpu/razor_cpu.cpp); the NID and a stub returning 0 come from there, but its stub declares no arguments; the `const char* label` parameter (NUL-terminated, up to 16384 bytes) comes from [SharpProspero](https://github.com/SvenGDK/SharpProspero), which documents it as the frame-boundary label. Without a capture it does nothing and returns 0; the label is not read, and the capture behavior and the real return values are unverified +- [sceRazorCpuGetDataTagStorageSize](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - throws: [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Debug/RazorCpu.cs#L142-L144) documents it as the byte size a tag table needs for a count of entries, but not the size of an entry diff --git a/docs/dev/debt/libSceRemoteplay.md b/docs/dev/debt/libSceRemoteplay.md new file mode 100644 index 0000000000..a1bb852834 --- /dev/null +++ b/docs/dev/debt/libSceRemoteplay.md @@ -0,0 +1,43 @@ +# libSceRemoteplay technical debt + +### Unknown function info + +- [sceRemoteplayApprove](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayClearAllRegistData](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayClearConnectHistory](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayConfirmDeviceRegist](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayDisconnect](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayGeneratePinCode](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayGetApMode](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayGetConnectHistory](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayGetConnectUserId](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayGetMbusDeviceInfo](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayGetOperationStatus](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayGetPrimaryUserId](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayGetRemoteplayStatus](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayGetRpMode](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayImeClose](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayImeFilterResult](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayImeGetEvent](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayImeNotify](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayImeNotifyEventResult](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayImeOpen](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayImeSetCaret](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayImeSetText](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayIsRemoteOskReady](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayIsRemotePlaying](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayNotifyMbusDeviceRegistComplete](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayNotifyNpPushWakeup](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayNotifyPinCodeError](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayNotifySubmitCode](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayNotifyUserDelete](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayPrintAllRegistData](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayProhibit](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayProhibitStreaming](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayServerLock](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplayServerUnLock](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplaySetApMode](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplaySetLogLevel](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplaySetProhibition](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplaySetProhibitionForVsh](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) +- [sceRemoteplaySetRpMode](../../../core/libs/prx/libSceRemoteplay/Export.cpp) (libSceRemoteplay) - declared without parameters, signature unknown; name from the PS5 3.20 export inventory (DNNDHH/PS5-3.20_Libs) diff --git a/docs/dev/debt/libSceRtc.md b/docs/dev/debt/libSceRtc.md index 39dc328d7f..cf5399b601 100644 --- a/docs/dev/debt/libSceRtc.md +++ b/docs/dev/debt/libSceRtc.md @@ -6,11 +6,16 @@ ### Unknown function info -- [sceRtcParseDateTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - the console's zone name table is only known from the names measured on PS5 firmware 13.60 and 13.42; any other name is assumed to take the military letter offset of its first character, as every measured name outside the table does (`HST`, `MSK`, `XYZ`), so a name the console's table holds but nobody measured gets the wrong offset -- [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) +- [sceRtcParseDateTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - the zone name table matches every name of 1 to 3 uppercase letters as a payload process on PS5 firmware 13.60 parsed them in the RFC 2822 form; of the longer names only real ones were tried (ACDT, AEST, CEST, NZDT, MESZ and others). Any other name of 4 or more letters is assumed to take the military letter offset of its first character, so a longer name the console's table holds but nobody measured gets the wrong offset +- [sceRtcParseDateTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - a blank zone after seconds is accepted. With fully allocated input storage on PS5 firmware 13.42 and again on 13.60, the same NUL-terminated text returned `BAD_PARSE` when the byte after its terminator was zero and succeeded when that byte was uppercase `T`. This dependence on storage after the terminator is not modeled; controlled backing-byte measurements in a title process are unavailable. +- [sceRtcFormatRFC2822, sceRtcFormatRFC2822LocalTime, sceRtcFormatRFC3339LocalTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - only shadPS4 (PS4) has bodies. A null tick formats the current time, as shadPS4 does and as these three and `sceRtcFormatRFC3339` did in a payload process on a PS5 (firmware 13.60); offsets outside ±1439 minutes return `INVALID_VALUE` like `sceRtcFormatRFC3339`, also with a null tick there; 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 38a4add132..19757221c0 100644 --- a/docs/dev/debt/libSceRudp.md +++ b/docs/dev/debt/libSceRudp.md @@ -2,13 +2,15 @@ ### Silent stubs -- [sceRudpProcessEvents and sceRudpNetFlush](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - there is no reliable-UDP stack on the host, and every `sceRudpWrite`, `sceRudpRead` and `sceRudpGetStatus` call here reports a reset or idle context without queueing data, so no context ever holds pending data to process or send; both calls return success after `sceRudpInit` and `RUDP_ERROR_NOT_INITIALIZED` before it, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/rudp/rudp.cpp) returns OK from its stubs. PPSA03490 calls `sceRudpProcessEvents(0)` followed by `sceRudpNetFlush()` from its P2P service thread and ignores the first result; whether the real calls take parameters is unknown +- [sceRudpProcessEvents, sceRudpNetFlush and sceRudpNetReceived](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - there is no reliable-UDP stack on the host, and every `sceRudpWrite`, `sceRudpRead` and `sceRudpGetStatus` call here reports a reset or idle context without queueing data, so no context ever holds pending data to process, send or receive; the three calls return success after `sceRudpInit` and `RUDP_ERROR_NOT_INITIALIZED` before it, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/rudp/rudp.cpp) returns OK from its stubs. What the console's `sceRudpNetReceived` returns is unverified. PPSA03490 calls `sceRudpProcessEvents(0)` followed by `sceRudpNetFlush()` from its P2P service thread and ignores the first result; whether the real calls take parameters is unknown +- [sceRudpEnableInternalIOThread2](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) takes `(uint32_t stack_size, uint32_t priority, uint64_t affinity)`, taken from the managed `SceRudp.Rudp.EnableInternalIOThread(uint stackSize, uint priority, ulong affinity)` in PPSA06888's Unity metadata; the first two match `sceRudpEnableInternalIOThread`. It returns success after `sceRudpInit` and `RUDP_ERROR_NOT_INITIALIZED` before it, as `sceRudpEnableInternalIOThread` does, without starting any thread. Whether the console reports other errors, and whether it applies the affinity, is unverified. - [sceRudpActivate](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - there is no reliable-UDP stack on the host to activate, so it returns success without doing anything, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/rudp/rudp.cpp) does for its own `sceRudpActivate`. PPSA01325 calls it with no arguments during start-up; what the console activates, and whether it reports an error before `sceRudpInit`, is unverified ### Unknown function info - [sceRudpGetStatus](../../../core/libs/prx/libSceRudp/Export.cpp) - PPSA01521 passes an output pointer and byte count `0xF8`. Measured on a PS5 (firmware 13.60): the status is 0xF8 bytes; before `sceRudpInit` every call returns `RUDP_ERROR_NOT_INITIALIZED`, and after it a null pointer or a byte count of 0 or above 0xF8 returns `RUDP_ERROR_INVALID_ARGUMENT`. The layout matches `SceRudpStatus` in [Vita3K](https://github.com/AndnixSH/Vita3K-PS-Vita-OS-Fork/blob/master/vita3k/modules/SceRudp/SceLibRudp.cpp) with `currentContexts` at 0x64 and `allocs`, `frees`, `memCurrent` and `memPeak` at 0xC8 to 0xD4. Only `currentContexts` is reported: the traffic counters stay 0 because nothing is sent, and the allocator counters stay 0 because the library does not allocate from the title's pool (the console reports 6 allocations and 0x1260 bytes right after `sceRudpInit`, and 4 more allocations and 0x3F0 bytes per context). The console also writes `currentContexts` and the allocator counters when the byte count is smaller than their offset; only the requested bytes are written here. - [sceRudpGetMaxSegmentSize](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - measured on a PS5 (firmware 13.60) in #3073: before `sceRudpInit` it returns `RUDP_ERROR_NOT_INITIALIZED` and leaves the value untouched, also for a null pointer; after it a null pointer returns `RUDP_ERROR_INVALID_ARGUMENT` and a valid one gets 1410 with result 0. The library keeps no configurable segment size (there is no `sceRudpSetMaxSegmentSize`), so 1410 is always reported. 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 -- PPSA06888 imports declared without parameters, signatures unknown: [sceRudpEnableInternalIOThread2, sceRudpGetOption, sceRudpGetRemoteInfo](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp), imported by its bundled `Media/Plugins/SceRudp.prx` and not called during start-up. 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) - +- [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 +- [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.md b/docs/dev/debt/libSceSaveData.md new file mode 100644 index 0000000000..936ec9f874 --- /dev/null +++ b/docs/dev/debt/libSceSaveData.md @@ -0,0 +1,5 @@ +# libSceSaveData technical debt + +### Unknown function info + +- The following pending declarations have unverified signatures and behavior; they are parameterless import-resolution declarations and throw through `NotImplemented_nid_no_patch`: [`sceSaveDataAbort`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataBindPsnAccount`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataBindPsnAccountForSystemBackup`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataChangeDatabase`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataChangeInternal`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataCheckBackupData`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataCheckBackupDataForCdlg`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataCheckBackupDataInternal`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataCheckBackupDataLight`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataCheckCloudData`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataCheckSaveDataBroken`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataCheckSaveDataVersion`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataCheckSaveDataVersionLatest`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataClearProgress`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataCreateUploadData`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataDebugGetThreadId`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataDeleteAllUser`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataDeleteBackupData`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataDeleteCloudData`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataDeleteProspero`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataDeleteTitle`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataDeleteUser`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataDirNameSearchInternal`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataDownload`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataGetAllSize`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataGetAppLaunchedUser`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataGetAutoUploadConditions`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataGetAutoUploadRequestInfo`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataGetAutoUploadSetting`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataGetBoundPsnAccountCount`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataGetClientThreadPriority`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataGetCloudQuotaInfo`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataGetEventInfo`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataGetMountedSaveDataCount`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataGetProgress`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataGetSaveDataCount`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataGetSaveDataRootDir`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataGetSaveDataRootPath`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataGetSaveDataRootUsbPath`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataGetSavePoint`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataGetUpdatedDataCount`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataInitialize2`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataInitializeForCdlg`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataIsDeletingUsbDb`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataIsMounted`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataManualDelete`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataManualDownload`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataManualUpload`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataMount5`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataMountInternal`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataMountSys`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataRebuildDatabase`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataRegisterEventCallback`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataRestoreBackupData`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataRestoreBackupDataForCdlg`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataRestoreLoadSaveDataMemory`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataSetAutoUploadSetting`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataSetEventInfo`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataSetSaveDataLibraryUser`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataShutdownStart`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataSupportedFakeBrokenStatus`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataSyncCloudList`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataUmountSys`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataUmountWithBackup`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataUnregisterEventCallback`](../../../core/libs/prx/libSceSaveData/Export.cpp), [`sceSaveDataUpload`](../../../core/libs/prx/libSceSaveData/Export.cpp). diff --git a/docs/dev/debt/libSceSaveData.native.md b/docs/dev/debt/libSceSaveData.native.md index ad4a4bf0b4..db7aeb7660 100644 --- a/docs/dev/debt/libSceSaveData.native.md +++ b/docs/dev/debt/libSceSaveData.native.md @@ -8,8 +8,10 @@ - [sceSaveDataConvert](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - the parameter block (user id at `+0x00`, `SceSaveDataDirName` pointer at `+0x08`, 48 opaque bytes after it) is read from PPSA28416's single call, not confirmed by KytyPS5, shadPS4, fpPS4 or SharpProspero, which have no body for it. Host saves have no size limit, so an existing save returns `SCE_OK` without touching its files and no event is queued; what the console returns for a save that is mounted, already converted or needs no conversion is unknown. The directory name is validated as `sceSaveDataDelete` does - [sceSaveDataGetConvertProgress](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - imported by PPSA16064; signature and error codes follow 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-get-convert-progress.md) (`float* progress`). Save data conversion on host filesystems completes synchronously, so the progress query returns `SAVE_DATA_OK` with the last conversion's progress (`0.0f` before any conversion or when conversion fails, `1.0f` after successful conversion), `SAVE_DATA_ERROR_PARAMETER` if `progress` is null, or `SAVE_DATA_ERROR_NOT_INITIALIZED` when the library is uninitialized; intermediate asynchronous conversion progress on console is unmeasured - [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 +- [sceSaveDataDirNameSearch](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - the sort keys (0 directory name, 1 user parameter, 2 blocks, 3 modification time, 5 free blocks) and orders (0 ascending, 1 descending) follow shadPS4, not a hardware run; saves with equal keys keep directory-name order before a descending reverse. The search infos (blocks, free blocks and 32 reserved bytes per returned name) follow shadPS4's `OrbisSaveDataSearchInfo`. Keys other than 0, 1, 2, 3 and 5 (including 4) and orders above 1 return `SCE_SAVE_DATA_ERROR_PARAMETER`; shadPS4 and KytyPS5 only reject keys above 5, so key 4 is rejected here because no source names it. In the directory name pattern `%` matches any run of characters, none included, and `_` exactly one, as the comments of shadPS4's matcher describe; also not confirmed on hardware +- [sceSaveDataDirNameSearch, sceSaveDataGetMountInfo, sceSaveDataMount3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - the 64 KiB block of [SharpEmu](https://github.com/sharpemu/sharpemu/blob/c6d304cc3328bad62cb671327ace181110cf2ddb/src/SharpEmu.Libs/SaveData/SaveDataExports.cs#L82-L84) and the 48 to 16384-block range of SharpEmu and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/569ec04d1a77127c601eed522ad5bba187173f9d/src/libs/libSaveData.cpp#L30-L31) are not measured on a console, and file system overhead is not counted. What the console does with a `blocks` outside that range is unknown: KytyPS5 rejects the creating mount and SharpEmu clamps it; here a larger one is kept as 16384 and a smaller non-zero one as given. A save's size is the `blocks` of the mount that created it, kept in `.blocks`, else that of its next mount that names one, else 16384; its free blocks are that size less its files, each rounded up to 64 KiB blocks - [sceSaveDataInitialize3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - a repeated initialize succeeds: PPSA12544's executable and its Unity SaveData plugin both initialize, and the plugin fails on any error. Initializations are counted and `sceSaveDataTerminate` ends the session at the last one; how the console pairs them is unknown +- [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 +19,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, sceSaveDataLoadIcon, sceSaveDataSaveIcon, sceSaveDataSetParam, sceSaveDataUmount2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - null pointer arguments return `SAVE_DATA_ERROR_PARAMETER` instead of throwing C++ exceptions; unmounted targets return `SAVE_DATA_ERROR_NOT_MOUNTED`, and parameter type or buffer size violations return `SAVE_DATA_ERROR_PARAMETER` after slot lookup - [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/libSceSdma.md b/docs/dev/debt/libSceSdma.md new file mode 100644 index 0000000000..78b6855431 --- /dev/null +++ b/docs/dev/debt/libSceSdma.md @@ -0,0 +1,5 @@ +# libSceSdma technical debt + +### Unknown function info + +- [libSceSdma pending exports](../../../core/libs/prx/libSceSdma/Export.cpp) - 26 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceSdma.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. 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 dadc3f4a07..e0bc2e09c2 100644 --- a/docs/dev/debt/libSceSystemGesture.md +++ b/docs/dev/debt/libSceSystemGesture.md @@ -2,4 +2,9 @@ ### Silent stubs -- [System gesture recognizers](../../../core/libs/prx/libSceSystemGesture/Export.cpp) (libSceSystemGesture) - `sceSystemGestureGetPrimitiveTouchEvents`, `GetPrimitiveTouchEventsCount`, `GetTouchEvents`, `GetPrimitiveTouchEventByIndex` and the recognizer append, remove, reset and update calls report that no touch input exists, so a title never receives a gesture event. Handle and pointer arguments are still checked across all query entry points including `sceSystemGestureGetTouchEventByIndex`. 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 874c9d09e7..2e396200ca 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -5,18 +5,103 @@ - [libSceSystemService](../../../core/libs/prx/libSceSystemService/Export.cpp) - `sceSystemServicePowerTick`, `sceSystemServiceReportAbnormalTermination`, `sceSystemServiceDisableMusicPlayer`, `sceSystemServiceReenableMusicPlayer`, `sceSystemServiceDisableMediaPlay` and `sceSystemServiceReenableMediaPlay` return success and do nothing: the host has no auto power-off timer to reset, no system crash reporter to notify, no system music player to pause or resume and no system media playback to disable. PPSA01521 calls `sceSystemServiceDisableMediaPlay` during start-up, and its wrappers for both pass no arguments - [sceSystemServiceLaunchWebBrowser](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - AnyPS5 launches no web browser, so the call fails and nothing is shown. RommPS PPSA76677 then tells the player to scan its QR code instead - [sceSystemServiceShowControllerSettings](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - returns success without showing the controller settings: AnyPS5 has no system UI to open. Cat Quest III (PPSA15065) imports it +- [sceSystemServiceShowDisplaySafeAreaSettings](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - returns success without showing the display safe area settings dialog: AnyPS5 has no system UI to open. PPSA03332 imports it, and the no-argument signature is assumed: [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json) has `sceSystemServiceShowControllerSettings` but not this call - [sceSystemServiceOpenChallengeActivity](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - returns success without opening the challenge activity: AnyPS5 has no system UI to open. Cat Quest III (PPSA15065) imports it +- [sceSystemServiceOpenTournamentOccurrence](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - returns success without opening the tournament occurrence: AnyPS5 has no system UI to open ### Unknown function info +- [sceSystemServiceAddLocalProcessForPsmKit, sceSystemServiceKillLocalProcessForPsmKit](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - AnyPS5 runs one process and starts or stops no other, so both throw without reading their arguments. The parameters follow Forever Launcher's own firmware 5.50 probes (`ps5/probes/localproc` in [bizkut/ps5-forever](https://github.com/bizkut/ps5-forever)): a path, a null-terminated argv and a 0x90-byte parameter block in, a handle out; the kill takes that handle. Their results on the console are unknown. Forever Launcher (PPSA99030) imports both and stops at load without them. - [sceSystemServiceLaunchApp](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - AnyPS5 runs one title and cannot start another, so every call throws without reading its arguments. The (title ID, null-terminated argv, launch parameters) signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/SystemService/SystemService.cs#L141-L148), whose `AppLauncher` Prospero Explorer (PPSA99109) and Prospero Multi Tools (PPSA99110) import it through and which passes a 0x20-byte launch context or null; the console's argument checks and errors are unknown - [sceSystemServiceLaunchWebBrowser](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the console's error for an unavailable web browser is unknown; it returns the kernel's `EOPNOTSUPP` (0x8002002D) for any arguments. The (uri, param) signature follows [RommPS](https://github.com/s0liton/RommPS/blob/754d4d60f25938a5136b9e2efcd074313c4386c5/platform/ps5/app/rommps/examples/rommps/kit/credits.cpp#L86-L95), which passes a URL and a null param and only tests the result for zero; the param layout and the console's argument checks are unknown -- [sceSystemServiceParamGetString](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the system name (parameter 6) is the fixed `PS5`, not the console's name; other parameters and buffers of 1 to 64 bytes throw. PPSA26344 at `0xA87935` passes parameter 6 with a 65-byte buffer +- [sceSystemServiceParamGetString](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the system name (parameter 6) is the fixed `PS5`, not the console's name. As in a payload process on a PS5 (firmware 13.60), the name is written NUL-padded to the whole buffer, and a buffer under 65 bytes, a null buffer or any other parameter returns `SYSTEM_SERVICE_ERROR_PARAMETER` without writing. Parameters 0 to 40, 100 to 110, 1000 to 1010, -1 and `INT_MAX` were tried there; the others are assumed to fail the same way. PPSA26344 at `0xA87935` passes parameter 6 with a 65-byte buffer - [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - console language numbers 0 to 29 follow the PS4 SDK enum in [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/06fcf7fd108fa080035cdd3259ed22eb07e0508c/include/orbis/_types/sys_service.h#L54-L83) (21 Arabic). 30 (Ukrainian) is not in it and follows PPSA99505 ([Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/app/i18n.cpp#L29-L60)) and PPSA99002 ([ProsperoLight](https://github.com/blackbearreloaded/ProsperoLight/blob/b708bbde3457bef1073a9ab6959957cb691e8e2f/include/i18n.hpp#L252-L309)), whose numbers agree with the enum. Not confirmed on a console, and neither title starts on `main` yet. Parameter 400 returns the same number as parameter 1: PPSA01325 asks for 400 first, asks for 1 when that fails, and uses either answer as an index into the same 31 languages; in the PS5 10.01 and 12.60 `libSceSystemService` modules parameter 400 reads the same registry value as parameter 1, without parameter 1's mapping of newer language numbers by SDK version, and returns 0x80a10003 when an imported check that is not identified returns 1; the 4.03 module has no parameter 400. Neither the mapping nor that error is modelled, and the parameter's name is not known. Without `ANYPS5_LANGUAGE` the language is English (United States); the date format, time format and enter button stay fixed - [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the time zone (parameter 4) and summer time (parameter 5) follow the host's local time zone at the moment of the call. Titles compute the local time as UTC plus the time zone in minutes plus 60 minutes when summer time is 1 ([Apollo](https://github.com/bucanero/apollo-ps4/blob/252a9d5a187c463994263009851a179f4f910c49/source/exec_cmd.c#L70-L92), [GoldHEN's frame logger](https://github.com/GoldHEN/GoldHEN_Plugins_Repository/blob/6c3a06f53d9caebadfb0aeed9bb94119012010a7/plugin_src/frame_logger/source/main.c#L60-L82), and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/413d2e3518fb435f40971d238fe0ecae7ee551da/src/SharpProspero/Platform/SystemParameters.cs#L75-L79) calls them the offset from UTC in minutes and whether summer time is in effect), so the time zone is the host's current offset east of UTC less the summer time shift while summer time is in effect: 60 minutes on Linux, and on Windows the C runtime's own summer time bias (`_get_dstbias`), which is also what shifted its local time. For a host zone whose summer time is not one hour, Linux keeps the sum right and the standard offset wrong, and Windows the opposite; how the console reports such a zone is unknown. libkernel's `sceKernelConvertUtcToLocaltime`, `sceKernelConvertLocaltimeToUtc` and `sceKernelGettimezone` still use UTC +- [sceSystemServiceInitializePlayerDialogParam](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - a null param throws: what the console does with it is not measured. The console run in [prosper](https://github.com/mattias800/prosper/blob/40d95db32efe1ad00106a5b9a84e018fffa9ecff/prosper/tests/data/console_oracle/nethttp.golden.tsv#L46) shows only the 0x40 bytes written (size 0x40, user ID -1 at 0xC, zeros), so the field names and the mode and target account fields follow [SharpProspero's struct](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/SystemService/SystemService.cs#L637-L655), and the `void` return [its declaration](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/SystemService/SystemService.cs#L419-L426); the console left 0x800693020 in `rax`, not an SCE error code. Closes when: the null case is measured on a PS5 + +- [libSceSystemService firmware 3.20 pending exports](../../../core/libs/prx/libSceSystemService/PendingExports.cpp) - 475 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`. +- [sceSystemServiceLaunchSystemDeeplink](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilLaunchByUriParamInit](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceSystemStateMgrTickVideoPlayback](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceSystemServiceTelemetrySetData2WithUserId](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilStartUmmDictation](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceSystemServiceGetSpotifyTitleId](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceSystemServiceGetAppStatusListContainsDaemon](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceSystemServiceSwitchToVRApp](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceSystemServiceSwitchToNonVRApp](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceLncUtilGetSpmemAllocatorConfigValue](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceCsbMgrInitialize](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceCsbMgrTerminate](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceCsbMgrCreateLocalSandbox](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceCsbMgrDestroyLocalSandbox](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceCsbMgrSwitchLocalSandbox](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceCsbMgrGetCurrentLocalSandboxId](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceCsbMgrGetLocalSandboxIdList](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceCsbMgrGetLocalSandboxInfo](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceCsbMgrSetLocalSandboxInfo](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceCsbMgrIssueStreamingSandboxInfo](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceReportSequenceHandlerRequestReport](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceReportSequenceHandlerCancelReport](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceReportSequenceHandlerExistsEvent](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceReportSequenceHandlerUpdateEventCache](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceReportSequenceHandlerGetEventCache](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceLncUtilRegisterCallEntries](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceLncUtilGetSpmemConfigJson](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceLncUtilExportSpmemConfigJson](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceLncUtilImportSpmemConfigJson](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilExitAppWithValue](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilNpDebugSpecialNotification](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilIsDddTemperatureDanger](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilIsM2StorageTemperatureDanger](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilGetAvailableSizeM2](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilNotifyConnectAccountStatusChanged](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilIsInternalCronosUser](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilChangePbtcMode](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilTelemetryCacheData](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilMicMuteKeyPressed](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilSwitchMicMute](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilNotifyStartEventToUniversalMicMute](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilIsLotusMicActivated](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilResetAllNoticeScreenSkipFlag](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilResetNoticeScreenSkipFlag](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilGetNoticeScreenSkipFlagByAppId](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilGetPasscodeInputType](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilGetPasscodeDisplayInfo](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilNotifySystemResumeTelemetryInfo](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilUpdateTelemetryToken](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilGetVRBaseMatRectangles](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilSetVRBaseMatRectangles](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilIsLaunchBlockDialog](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilGetConsoleToken](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilGetDeviceIdByDeviceUniqueId](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilGetDeviceUniqueIdByDeviceId](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilGetStreamingClientInfo](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilRequestCffUpdate](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilGetCffConfig](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilRemoveCffConfigFile](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilDebugGetNumberOfCffItems](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilDebugGetCffInfo](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilDebugSetCffStatus](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilGetPeripheralConnectionSessionId](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilGetCdlgEnabledFreezeRendering](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilGetNpTitleId](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilIsCronosInGameCommerceEnabled](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilLemSettingGet](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilLemSettingSet](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilLemSettingGetList](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilLemSettingReset](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceShellCoreUtilIsEChassisVariant](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceSystemStateMgrGetRebootCause](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceCoredumpSequenceHandlerUpdateEventCache](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceCoredumpSequenceHandlerGetEventCache](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceCoredumpSequenceHandlerGetWorkingEventSummary](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceCoredumpSequenceHandlerGetCoredumpStatus](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceCoredumpSequenceHandlerGetCoredumpStatusByAppId](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature +- [sceCoredumpSequenceHandlerCancelCoredump](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature ### 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 - [Running application ID](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - `sceSystemServiceGetAppIdOfRunningBigApp` reports 1 as the running title's app ID. The value is AnyPS5's own: the console's shell assigns app IDs and their values are not known here, so other calls that take an app ID must use the same one. `sceSystemServiceKillApp` with that ID and the arguments -1, 0 and 0, which [SwanStationPS5](https://github.com/darkxex/SwanStationPS5/blob/4cdc981d1a64ffb7f2af73f7a27a9e91a3cb5eb4/src/main.c#L1050-L1058) passes to close itself and [ps5-payload-dev's hbldr](https://github.com/ps5-payload-dev/websrv/blob/46374edee6627885abac717de24da7a1d73af6f1/src/ps5/hbldr.c#L425-L435) passes to close the running game, ends the title as its own `exit(0)` would: the guest's shutdown and `atexit` handlers run and the process exits with status 0. On the console the call returns and the shell then ends the title, and whether those handlers run there is unknown; here there is no shell, so the call does not return. Another app ID or other arguments throw. +- [sceLncUtilKillApp, sceLncUtilKillAppWithReason](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the running title's app ID ends the title as `sceSystemServiceKillApp` does, which [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/SystemService/SystemService.cs#L219-L227) describes as a wrapper of `sceLncUtilKillApp`; the signatures follow its [launcher utility bindings](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/SystemService/SystemService.cs#L262-L279). Another app ID throws, and so does a reason other than 0, whose meaning and effect on the console are not documented - [LoadExec descriptors](../../../core/libs/prx/libSceSystemService/LoadExec.cpp) (libSceSystemService) - on Linux, a call with an argument list replaces the process with `fexecve`, which first closes every descriptor above 2, and on macOS with `posix_spawn`, which closes them through `POSIX_SPAWN_CLOEXEC_DEFAULT`; a call without one uses `execv`, and the guest's descriptors stay open across it. On both paths descriptor 2 first gets back the host standard error a guest redirect kept, so the runtime's output does not follow the title's redirect (RetroArch's trace file, VLC's log) into the next executable. Which descriptors the console passes to the new process is not known diff --git a/docs/dev/debt/libSceTEEClient.md b/docs/dev/debt/libSceTEEClient.md new file mode 100644 index 0000000000..04823e75e6 --- /dev/null +++ b/docs/dev/debt/libSceTEEClient.md @@ -0,0 +1,5 @@ +# libSceTEEClient technical debt + +### Unknown function info + +- [libSceTEEClient pending exports](../../../core/libs/prx/libSceTEEClient/Export.cpp) - 10 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceTEEClient.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. diff --git a/docs/dev/debt/libSceUlt.md b/docs/dev/debt/libSceUlt.md index 95e0067125..b55da9ee7f 100644 --- a/docs/dev/debt/libSceUlt.md +++ b/docs/dev/debt/libSceUlt.md @@ -2,13 +2,18 @@ ### 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, sceUltAbstractThreadFactoryCreateThread, sceUltAbstractThreadFactoryGetNumMaxThreads, sceUltAbstractThreadFactoryJoinThread, sceUltKernelThreadFactoryFinalize, sceUltKernelThreadFactoryGetFactoryPointer, sceUltKernelThreadFactoryInitialize, sceUltModuleFinalize, sceUltModuleInitialize, sceUltUlthreadFactoryFinalize, sceUltUlthreadFactoryGetFactoryPointer, sceUltUlthreadFactoryGetWorkAreaSize, sceUltUlthreadFactoryInitialize, sceUltConditionVariableCreate, sceUltConditionVariableOptParamInitialize](../../../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 +- [sceUltMutexTryLock, the reader-writer lock and sceUltUlthreadGetSelf, sceUltUlthreadYield and sceUltUlthreadExit](../../../core/libs/prx/libSceUlt/src/ReaderWriterLock.cpp) (libSceUlt) - the signatures follow the PS Vita libult in [RPCS3](https://github.com/RPCS3/rpcs3/blob/13aa88c3c4d66ea8799e987f7d762d90943ab17b/rpcs3/Emu/PSP2/Modules/sceUlt.cpp) (a 256-byte lock and 128-byte option structures), not a PS5 binary. The rules are assumed: readers may take the lock while a writer waits, a thread that holds the write lock gets `ULT_ERROR_STATE` for any further lock instead of waiting on itself, a reader asking for the write lock waits on its own read lock, and the try forms return `ULT_ERROR_BUSY`. Outside an ulthread, `sceUltUlthreadGetSelf`, `sceUltUlthreadYield` and `sceUltUlthreadExit` return `ULT_ERROR_STATE`; inside one they act on the guest thread it runs on, since ulthreads are not scheduled cooperatively here ### Functional - [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`. - [sceUltWaitingQueueResourcePoolDestroy](../../../core/libs/prx/libSceUlt/Export.cpp) refuses with `ULT_ERROR_BUSY` only when a thread is currently waiting on a pool-backed mutex, semaphore or queue, matching the PS5 libSceUlt disassembly (0x473a in or55417wcDk); bound but idle objects do not block pool destruction. Waiting-queue pool guest work area node accounting is not implemented. - [sceUltMutexDestroy](../../../core/libs/prx/libSceUlt/Export.cpp) returns `ULT_ERROR_BUSY` when the mutex is held or has waiting threads, tracked via owner and waiter counts in `UltMutexState`. Prosper (`hle_ult.cpp`) logs a destroy of a held mutex and returns `ULT_OK`; AnyPS5 returns `ULT_ERROR_BUSY` to prevent destroying active mutexes. + +- The following pending declarations have unverified signatures and behavior; they are parameterless import-resolution declarations and throw through `NotImplemented_nid_no_patch`: [`_sceUltConditionVariableOptParamInitialize`](../../../core/libs/prx/libSceUlt/Export.cpp). diff --git a/docs/dev/debt/libSceUrlConfigResolver.md b/docs/dev/debt/libSceUrlConfigResolver.md new file mode 100644 index 0000000000..035bdc8e88 --- /dev/null +++ b/docs/dev/debt/libSceUrlConfigResolver.md @@ -0,0 +1,5 @@ +# libSceUrlConfigResolver technical debt + +### Unknown function info + +- The following pending declarations have unverified signatures and behavior; they are parameterless import-resolution declarations and throw through `NotImplemented_nid_no_patch`: [`sceUrlConfigResolverGetReleaseVersion`](../../../core/libs/prx/libSceUrlConfigResolver/Export.cpp), [`sceUrlConfigResolverGetSysVer`](../../../core/libs/prx/libSceUrlConfigResolver/Export.cpp), [`sceUrlConfigResolverIsUpdated`](../../../core/libs/prx/libSceUrlConfigResolver/Export.cpp), [`sceUrlConfigResolverUpdate`](../../../core/libs/prx/libSceUrlConfigResolver/Export.cpp). 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/libSceUserService.md b/docs/dev/debt/libSceUserService.md index f159b8f893..4fca560df4 100644 --- a/docs/dev/debt/libSceUserService.md +++ b/docs/dev/debt/libSceUserService.md @@ -3,7 +3,7 @@ ### Unknown function info - [sceUserServiceGetNpAccountId](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - the account id for a user with no linked PSN account is assumed to be 0 with success; libSceNpManager's `sceNpGetAccountIdA` answers the same question with `SCE_NP_ERROR_SIGNED_OUT` -- [sceUserServiceInitialize, sceUserServiceTerminate](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - measured on a PS5 (firmware 13.60): before `sceUserServiceInitialize`, and again after `sceUserServiceTerminate`, `sceUserServiceGetLoginUserIdList`, `sceUserServiceGetEvent` and `sceUserServiceTerminate` return `0x80960002`; a second initialization returns `0x80960003`, and a new initialization reports the login of the initial user again. The other calls were not measured before initialization and do not check it. `sceUserServiceInitialize2` was not measured: it initializes like `sceUserServiceInitialize` and returns success when already initialized +- [sceUserServiceInitialize, sceUserServiceTerminate](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - not measured: whether an invalid thread priority leaves an uninitialized service uninitialized (it does here), the parameters after their first `int`, and any second argument of `sceUserServiceInitialize2`. Measured on a PS5: before initialization and after `sceUserServiceTerminate`, `GetLoginUserIdList`, `GetEvent` and `Terminate` return `0x80960002`; a second initialization returns `0x80960003` and a new one reports the initial user's login again; a priority outside 0x100 to 0x2FF, from the first `int` of the parameters or the `sceUserServiceInitialize2` argument, returns `0x80960005` before the initialized check. The other calls do not check initialization - [sceUserServiceGetUserColor](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - the default profile colour is assumed to be blue (0) - [sceUserServiceGetUserName](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - `0x80960105`, which a PS5 (firmware 13.60) returns for a user id it does not know, has no known name; it is `USER_SERVICE_ERROR_UNKNOWN_USER` here. On the console a registered user who is not logged in still gets their name; here only the initial user exists - [sceUserServiceGetAgeLevel](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - on a PS5 (firmware 13.60) the value is the age set in the user's parental restriction (0, 7, 12 and 15 were measured) and `0xFFFFFFFF` without one; a user who is not logged in returns `0x80960009` with or without an output. Parental restrictions are not modelled, so the initial user is always reported as unrestricted. PPSA12544 (Unity) only stores the value for its user profile diff --git a/docs/dev/debt/libSceVideoCoreInterface.md b/docs/dev/debt/libSceVideoCoreInterface.md new file mode 100644 index 0000000000..f5053cfc00 --- /dev/null +++ b/docs/dev/debt/libSceVideoCoreInterface.md @@ -0,0 +1,5 @@ +# libSceVideoCoreInterface technical debt + +### Unknown function info + +- The following pending declarations have unverified signatures and behavior; they are parameterless import-resolution declarations and throw through `NotImplemented_nid_no_patch`: [`sceVideoCoreIfInitializeInterface`](../../../core/libs/prx/libSceVideoCoreInterface/Export.cpp), [`sceVideoCoreIfMainLoop`](../../../core/libs/prx/libSceVideoCoreInterface/Export.cpp), [`sceVideoCoreIfSetFunctionTable`](../../../core/libs/prx/libSceVideoCoreInterface/Export.cpp). diff --git a/docs/dev/debt/libSceVideoOut.md b/docs/dev/debt/libSceVideoOut.md index ae73a5efc5..8b62870d99 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. @@ -16,7 +17,7 @@ - [sceVideoOutRegisterBuffers2](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - buffer option 8 (STRICT_COLORIMETRY) is presented as option 0, with the stored values unchanged; what the console changes for it is unknown. Only options 0 and 8 are accepted - [sceVideoOutAddBufferAttributeOption](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut, NID `MBlv8YWoyOk`) - the signature `(VideoOutBufferAttribute2*, uint64_t option)` comes from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/VideoOut/VideoOut.cs#L174-L176) and the call site in PPSA25380, which passes 0x20 and ignores the result. The option is ORed into `attribute->option`; whether the console validates the bits, keeps them elsewhere or fails on an unknown option is unknown. A null attribute throws. Option 0x20 makes `sceVideoOutRegisterBuffers2` throw, as every option other than 0 and 8; what it selects for an overlay buffer is unknown - [sceVideoOutRegisterBuffers2](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - DCC buffers (category 1): `dcc_control` is taken as the CB_DCC_CONTROL block layout, only bits 0x10026c are accepted; `dcc_cb_register_clear_color` as the register-clear texel, only 32-bit values are presented -- [sceVideoOutIsOutputSupported](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - mode 1 is supported; mode 15 is supported when the current host display runs at 120 Hz or above. Configuring mode 15 requires attribute3 bit 0x40; this single-bit mask is inferred from the measured full value 0x80040, not isolated by a console measurement. Vblanks run at 119.88 Hz until mode 1 restores 59.94 Hz. Display modes are read through SDL; a move to a slower display rejects further high-frame-rate presentation. Selecting another host display mode and VRR are not implemented. The console answers per display and per title: with a 120 Hz TV it also answers 1 for mode 15 (119.88 Hz), and [PS5_Vulkan](https://github.com/mihawk-99/PS5_Vulkan/blob/5b5e4fc2d80fdb67a4f61ba9e6a8a424026bb8a5/docs/HARDWARE_FINDINGS.md) measured that `sceVideoOutConfigureOutput` accepts 15 only from a title whose param.json declares high-frame-rate output (attribute3 0x80040). Modes 4, 7, 8, 12-14, 16-17 and 19 return `VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE` here, as they did on the measured display. Mode 18 throws, because one console answered 0x80290016 and another 0x8029001E. The name `VIDEO_OUT_ERROR_UNKNOWN_OUTPUT_MODE` for 0x8029001E is ours. Word 3 of the output options is accepted with any value, as on the console, and its meaning is unknown. PPSA12544 probes `0xd000000a`, which the console answers with 0x8029001E, so the title selects 3840 pixels; PPSA26344 at `0xCD0946` passes `(handle, 0xf, nullptr, nullptr, 0)` and branches on the result equalling 1 +- [sceVideoOutIsOutputSupported](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - mode 1 is supported; mode 15 is supported when the current host display runs at 120 Hz or above. Configuring mode 15 requires attribute3 bit 0x40; this single-bit mask is inferred from the measured full value 0x80040, not isolated by a console measurement. Vblanks run at 119.88 Hz until mode 1 restores 59.94 Hz. Display modes are read through SDL; a move to a slower display falls back the configured output mode to mode 1, fires the output-mode event and continues at 59.94 Hz. The console's exact fallback behaviour when a 120 Hz display is lost has not been measured; this fallback avoids the abort in #7877 and matches the HDMI output mode renegotiation that a display change requires. Selecting another host display mode and VRR are not implemented. The console answers per display and per title: with a 120 Hz TV it also answers 1 for mode 15 (119.88 Hz), and [PS5_Vulkan](https://github.com/mihawk-99/PS5_Vulkan/blob/5b5e4fc2d80fdb67a4f61ba9e6a8a424026bb8a5/docs/HARDWARE_FINDINGS.md) measured that `sceVideoOutConfigureOutput` accepts 15 only from a title whose param.json declares high-frame-rate output (attribute3 0x80040). Modes 4, 7, 8, 12-14, 16-17 and 19 return `VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE` here, as they did on the measured display. Mode 18 throws, because one console answered 0x80290016 and another 0x8029001E. The name `VIDEO_OUT_ERROR_UNKNOWN_OUTPUT_MODE` for 0x8029001E is ours. Word 3 of the output options is accepted with any value, as on the console, and its meaning is unknown. PPSA12544 probes `0xd000000a`, which the console answers with 0x8029001E, so the title selects 3840 pixels; PPSA26344 at `0xCD0946` passes `(handle, 0xf, nullptr, nullptr, 0)` and branches on the result equalling 1 - [sceVideoOutGetResolutionStatus](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the 48-byte layout follows the declarations in ps5-opengl and EVO-PLAYER-PS5 (#1578), which agree; not confirmed on a console. `screenSizeInInch` is 0, the value EDID uses for an undefined size, since the host display's size is not read; `flags` is 0, its bits are unknown. The pane equals the full size because `sceVideoOutSetWindowModeMargins` applies no margins - [sceVideoOutOpen](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the open param's first word is unknown (PPSA21564 passes 16; it is not the byte size, since the affinity mask is at offset 16); only 16 is accepted. PPSA26344's only call at `0xCCF5EA` passes `(0xFF, 0, 0, nullptr)`. Whether a non-null param continues past offset 24 is also unknown - [sceVideoOutGetBufferLabelAddress](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) exposes a port-owned contiguous array of 16 aligned 64-bit labels and returns its count. Astro Bot PPSA21564's bundled AGC driver passes a pointer output and addresses each label at `base + index * 8`. The count, zero initialization on registration, and release of the previous displayed buffer when its replacement completes follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/23c41333822fe8eca4c609b43119180af63ae4ac/src/core/libraries/videoout/video_out.cpp) and its driver; PS5 console equivalence has not been measured. Labels live until the port closes, and reopening creates a new array. Error precedence follows the reference for null output and the existing open-port policy for handles. Exact scanout release timing, repeated same-buffer flips and concurrent guest label stores remain unverified. EOP-flip registration and interrupt tokens remain unimplemented; exposing these labels does not implement that separate API. @@ -28,8 +29,11 @@ ### 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. - [HDR output](../../../core/libs/prx/libSceVideoOut/src/HostHdrProbe.cpp) (libSceVideoOut, libSceSystemService) - `sceVideoOutGetOutputStatus` reports dynamic range 2 and flag bit 0 (named HDR in [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/VideoOut/VideoOutTypes.cs)) only on Windows, while the window's Vulkan surface offers a 10-bit HDR10 swapchain with `VK_EXT_swapchain_colorspace` and `VK_EXT_hdr_metadata` and DXGI (`IDXGIOutput6::GetDesc1`) reports the window's monitor in `DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020`; `sceSystemServiceGetHdrToneMapLuminance` then reports the monitor's peak, full-frame peak and black level. A monitor that reports no usable peak gets a default of 1000 nits peak and full frame, 0 nits black. On Linux and other hosts the output stays SDR: a surface can offer HDR10 formats while the display is not in HDR (KDE Wayland with RADV lists them with the output's HDR disabled), and SDL2 and Vulkan expose no output HDR state; a compositor or output-state signal is needed first. The surface formats are read again when DXGI's monitor report changes and when the swapchain is recreated; a compositor that changes them without making the swapchain out of date is not seen until then. Every open port's output mode event fires when HDR output becomes available or unavailable and when its luminance changes; the console's events for an HDR change are unknown. Detection starts when VideoOut is first used: `sceSystemServiceGetHdrToneMapLuminance` called before that reports SDR, and no event fires for the availability found at start-up beyond the one every output-mode subscription gets when it is made. Console HDR settings (calibrated peak, the SDR-in-HDR conversion block) are not modelled -- [Windows game controllers](../../../core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp) (libSceVideoOut) - SDL's DirectInput joystick backend is disabled at default hint priority before `SDL_InitSubSystem`, because its device enumeration blocks forever on some vendor HID devices (#1042). XInput and HIDAPI controllers are detected; controllers reachable only through DirectInput are not. Setting `SDL_DIRECTINPUT_ENABLED=1` restores them on hosts without such a device +- [Windows game controllers](../../../core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp) (libSceVideoOut) - SDL's DirectInput joystick backend is disabled at default hint priority before `SDL_InitSubSystem`, because its device enumeration blocks forever on some vendor HID devices (#1042). XInput and HIDAPI controllers are detected; controllers reachable only through DirectInput are not. Setting `SDL_DIRECTINPUT_ENABLED=1` restores them on hosts without such a device. SDL's Windows.Gaming.Input backend stays on by default. Setting `APS5_NO_WGI` turns it off at default hint priority, for hosts where it faults at `Windows.Gaming.Input.dll+0x10a84` a few seconds into every run: seen on Windows 10 Pro 22H2 (19045) with the Nefarius and Oculus virtual gamepad emulation buses installed and no physical controller connected. With it off, devices and features reachable only through Windows.Gaming.Input are lost: racing wheels, arcade sticks, flight sticks, HID pads without an HIDAPI driver, and Xbox One and Series trigger rumble. + +- The following pending declarations have unverified signatures and behavior; they are parameterless import-resolution declarations and throw through `NotImplemented_nid_no_patch`: [`sceDbgVideoOutAddOutputModeVr60Privilege`](../../../core/libs/prx/libSceVideoOut/Unimplemented.cpp). diff --git a/docs/dev/debt/libSceVideodec.md b/docs/dev/debt/libSceVideodec.md new file mode 100644 index 0000000000..c24087ab78 --- /dev/null +++ b/docs/dev/debt/libSceVideodec.md @@ -0,0 +1,15 @@ +# libSceVideodec technical debt + +### Unknown function info + +- [sceVideodecCreateDecoder, sceVideodecDecode, sceVideodecDeleteDecoder, sceVideodecFlush and sceVideodecReset](../../../core/libs/prx/libSceVideodec/Export.cpp) (libSceVideodec) - the structure layouts and the error codes follow [shadPS4's PS4 library](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/videodec/videodec.h) and its [implementation](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/videodec/videodec.cpp), not measured on a PS5: the console's error for a rejected argument and its own version of the structures are unknown. The decoder runs through libSceVideodec2, which decodes H.264 with FFmpeg, and the picture is copied into the caller's frame buffer with the pitch aligned to 64 and the coded height, as the reference writes it + +- [libSceVideodec](../../../core/libs/prx/libSceVideodec/Export.cpp) (libSceVideodec) - the module's other two exports, `sceVideodecMapMemory` and `sceVideodecQueryResourceInfo`, are not provided, so a title that imports them fails to start + +### Functional + +- [sceVideodecCreateDecoder](../../../core/libs/prx/libSceVideodec/Export.cpp) (libSceVideodec) - the codec type is not checked and the decoder always decodes H.264, as the PS4 library does; the console's codec ids are not documented. A configuration without a maximum frame size is refused with the configuration error, and the resource information the caller passes is not used + +- [sceVideodecDecode and sceVideodecFlush](../../../core/libs/prx/libSceVideodec/Export.cpp) (libSceVideodec) - the picture information's video usability information (the tick rate, the sample aspect ratio and the colour description) stays zero, as the decoder does not report it, and the crop offsets are the decoded picture's crop halved, the unit the H.264 offsets use. The frame buffer holds the coded picture the decoder reports, so the rows the crop covers are in it and the caller crops them + +- [sceVideodecMapMemory and sceVideodecQueryResourceInfo](../../../core/libs/prx/libSceVideodec/Export.cpp) (libSceVideodec) - their names are listed in the PS5 3.20 export table and cross-checked against PS5 13.60. Their signatures and behaviour are unverified; these no-parameter declarations only resolve imports and throw through NotImplemented_nid_no_patch. diff --git a/docs/dev/debt/libSceVideodec2.md b/docs/dev/debt/libSceVideodec2.md index cf38f7d5d0..3706171e00 100644 --- a/docs/dev/debt/libSceVideodec2.md +++ b/docs/dev/debt/libSceVideodec2.md @@ -4,12 +4,14 @@ - [sceVideodec2GetAvcPictureInfo](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) - runs the `sceVideodec2GetPictureInfo` body (KytyPS5 binds both NIDs to one function; SharpProspero gives both the same `outputInfo, first, second` signature); the second (bottom field) picture info is never written, since only progressive H.264 output is modelled. prosper calls the AVC layout unestablished (no title observed calling it) +- [sceVideodec2TermLibrary and twelve other exports](../../../core/libs/prx/libSceVideodec2/Unimplemented.cpp) (libSceVideodec2) - report that they are not implemented; what they do on the console is not known. The others are `QueryLibraryInfo`, `QueryDecoderInfo`, `MapMemory`, `MapDirectMemory`, `PresetDecoderConfigInfo` (plain, `Avc`, `Hevc`, `Vp9`), `CreateDecoderBid`, `CreateHevcDecoder`, `QueryHevcDecoderMemoryInfo` and `GetHevcPictureInfo`. They are exported so that a title which imports them with `sceVideodec2InitLibrary` and calls none of them directly loads. Only `sceVideodec2MapDirectMemory` has a known parameter list, `(decoder, memory)`, from [EVO-PLAYER-PS5's `sce_videodec2.h`](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/9715f6ee9bf9b53461d6fdf9a2e11b4c48357783/projects/evoplayer/media/include/sce/sce_videodec2.h#L173); the others take none here + ### Functional - [AVC parameter-set units](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) - an H.264 access unit with no slice NAL (types 1 to 5), such as the sequence and picture parameter sets Twispero (PPSA99474) submits on their own before a stream's first slices, is kept and put in front of the next unit that has a slice, and `sceVideodec2Decode` returns 0 with no picture for it, since libavcodec refuses a packet without a slice. What the console returns for such a unit is not known. HEVC units of parameter sets alone are still passed to libavcodec as they are -- [sceVideodec2CreateDecoder](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) decodes HEVC (codec type 974921) and VP9 (2382845) through FFmpeg, besides H.264. The tags come from SharpProspero's `Videodec2CodecType` and EVO-PLAYER-PS5's `sce_videodec2.h`, which records them from hardware runs. HEVC input is converted from length-prefixed NAL units to Annex B as for H.264, and VP9 frames are passed through. Output is NV12 for 8-bit pictures. A 10-bit picture is written as 16-bit little-endian words with the sample in the low 10 bits, Y rows then interleaved UV rows at `framePitchInBytes` times the picture height, with `framePitch` in samples and `framePitchInBytes` in bytes (twice the width, aligned to 256), and `QueryDecoderMemoryInfo` sizes a frame of an HEVC or VP9 profile 2 configuration with two bytes per sample. Evidence is from [EVO-PLAYER-PS5](https://github.com/sainsaji/EVO-PLAYER-PS5/tree/9715f6ee9bf9b53461d6fdf9a2e11b4c48357783), which decodes and presents 1080p HEVC Main10 on hardware through this path (`docs/evo-pro/on-demand-decoder-handoff.md`): it takes a picture as 10-bit when `pitch_bytes >= pitch * 2`, puts UV at `pitch_bytes` times the height and samples with a low-bit factor (`projects/evoplayer/media/src/evo_vdec_native.c`, `ro_harvest`), and its hardware logs give 6128 KB and 24496 KB frame buffers for 1920x1088 and 3840x2176 Main10 decoders after rounding to 16 KiB. The 1080p size is what this formula gives; at 3840x2176 it gives 16 KiB less, so the console's size there has another term that is not known. Unverified: `frameFormat` (still 0; no consumer found reads it), VP9 profile 2 output (no hardware evidence), and the layout of 12-bit pictures and of decoded formats other than yuv420p10le, which throw. `sceVideodec2GetPictureInfo` and `sceVideodec2GetAvcPictureInfo` throw for HEVC and VP9 pictures, which use other layouts (`SceVideodec2HevcPictureInfo`, `SceVideodec2Vp9PictureInfo`), and `sceVideodec2GetHevcPictureInfo` is not exported; `sceVideodec2GetVp9PictureInfo` has its own VP9 layout +- [sceVideodec2CreateDecoder](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) decodes HEVC (codec type 974921) and VP9 (2382845) through FFmpeg, besides H.264. The tags come from SharpProspero's `Videodec2CodecType` and EVO-PLAYER-PS5's `sce_videodec2.h`, which records them from hardware runs. HEVC input is converted from length-prefixed NAL units to Annex B as for H.264, and VP9 frames are passed through. Output is NV12 for 8-bit pictures. A 10-bit picture is written as 16-bit little-endian words with the sample in the low 10 bits, Y rows then interleaved UV rows at `framePitchInBytes` times the picture height, with `framePitch` in samples and `framePitchInBytes` in bytes (twice the width, aligned to 256), and `QueryDecoderMemoryInfo` sizes a frame of an HEVC or VP9 profile 2 configuration with two bytes per sample. Evidence is from [EVO-PLAYER-PS5](https://github.com/sainsaji/EVO-PLAYER-PS5/tree/9715f6ee9bf9b53461d6fdf9a2e11b4c48357783), which decodes and presents 1080p HEVC Main10 on hardware through this path (`docs/evo-pro/on-demand-decoder-handoff.md`): it takes a picture as 10-bit when `pitch_bytes >= pitch * 2`, puts UV at `pitch_bytes` times the height and samples with a low-bit factor (`projects/evoplayer/media/src/evo_vdec_native.c`, `ro_harvest`), and its hardware logs give 6128 KB and 24496 KB frame buffers for 1920x1088 and 3840x2176 Main10 decoders after rounding to 16 KiB. The 1080p size is what this formula gives; at 3840x2176 it gives 16 KiB less, so the console's size there has another term that is not known. Unverified: `frameFormat` (still 0; no consumer found reads it), VP9 profile 2 output (no hardware evidence), and the layout of 12-bit pictures and of decoded formats other than yuv420p10le, which throw. `sceVideodec2GetPictureInfo` and `sceVideodec2GetAvcPictureInfo` throw for HEVC pictures (`SceVideodec2HevcPictureInfo` is not filled), and `sceVideodec2GetHevcPictureInfo` reports that it is not implemented -- [sceVideodec2GetVp9PictureInfo](../../../core/libs/prx/libSceVideodec2/Export.cpp) uses the 88-byte layout and two-pointer signature from [SharpProspero Videodec2.cs](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Video/Videodec2.cs). Frame metadata follows the [VP9 specification](https://storage.googleapis.com/downloads.webmproject.org/docs/vp9/vp9-bitstream-specification-v0.7-20170222-draft.pdf), sections 5.26 and 6.2: profile, key/intra-only flags, bit depth, color space/range, coded/render dimensions and superframe count are retained per emitted buffer. Level is 0 because the elementary stream does not signal a level; the console value is unverified. Null pointers, undersized structures and querying another codec throw; console error codes and support for other structure sizes are unknown. Pictures deeper than 10 bits are rejected. Superframes are submitted as individual coded frames to keep each output associated with its input metadata. +- [sceVideodec2GetVp9PictureInfo](../../../core/libs/prx/libSceVideodec2/Export.cpp) uses the 88-byte layout and two-pointer signature from [SharpProspero Videodec2.cs](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Video/Videodec2.cs). Frame metadata follows the [VP9 specification](https://storage.googleapis.com/downloads.webmproject.org/docs/vp9/vp9-bitstream-specification-v0.7-20170222-draft.pdf), sections 5.26 and 6.2: profile, key/intra-only flags, bit depth, color space/range, coded/render dimensions and superframe count are retained per emitted buffer. Level is 0 because the elementary stream does not signal a level; the console value is unverified. Null pointers, undersized structures and querying another codec throw; console error codes and support for other structure sizes are unknown. Pictures deeper than 10 bits are rejected. Superframes are submitted as individual coded frames to keep each output associated with its input metadata. `sceVideodec2GetPictureInfo` writes this layout for a VP9 picture, as shadPS4's `videodec2.cpp` writes the decoder's own layout; Mortal Shell 2 (PPSA34008) calls it with `thisSize` 88. - [sceVideodec2Decode after sceVideodec2Flush](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) without `sceVideodec2Reset` restarts FFmpeg's decoder (`avcodec_flush_buffers`) and decodes the unit, where it threw before. TEKKEN DARK RESURRECTION (PPSA31371), which runs in Sony's PSP emulator, decodes after a flush without a reset (#7118), so the console accepts the call. Pictures the flush decoded but has not returned yet are returned before the new ones. What the console returns for them, and for a stream that continues across the flush without a new IDR picture, is not measured. - [sceVideodec2Decode](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) and `sceVideodec2Flush` output the coded picture, with the chroma plane after its luma rows, as the console does for 1080p H.264 and HEVC streams coded at 1920x1088. An HEVC or VP9 picture whose size is not a multiple of 16, such as HEVC coded at 1920x1080, is output at that size; whether the console pads it is unverified. `sceVideodec2GetPictureInfo` reports no frame cropping diff --git a/docs/dev/debt/libSceVoice.md b/docs/dev/debt/libSceVoice.md index 5983d70f39..c6fee962c1 100644 --- a/docs/dev/debt/libSceVoice.md +++ b/docs/dev/debt/libSceVoice.md @@ -9,3 +9,16 @@ - [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 +- [sceVoicePausePortAll, sceVoiceResumePortAll](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - not measured on a PS5: both return 0x804E0801 while the library is not initialized and 0 otherwise, as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/c1b26558352b863f80142256e4df38585aa7d727/src/core/libraries/voice/voice.cpp#L356-L437); prosper reads the same code for `sceVoiceStart`, and the PS3 calls in [RPCS3](https://github.com/RPCS3/rpcs3/blob/be69c054eeac57e2e671697a463473d37fe5a7b2/rpcs3/Emu/Cell/Modules/cellVoice.cpp#L462-L543) make the same check. Like `sceVoicePausePort` they change nothing that is reported: [VitaSDK](https://github.com/vitasdk/vita-headers/blob/ae5808f3105c42fa3373ba4ab64ab54e2e3ca848/include/psp2/voice.h#L540-L557) documents that pausing every port keeps the buffered data and the reported state, and nothing here processes port data +- [sceVoiceInitHQ](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature +- [sceVoiceGetMemorySize](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature +- [sceVoiceDeviceInCustomerData](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature +- [sceVoiceQAEnableRecording](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature +- [sceVoiceVADAdjustment](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature +- [sceVoiceVADPVTAdjustment](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature +- [sceVoiceVADSetVersion](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature +- [sceVoiceSetMuteFlagAll](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature +- [sceVoiceSetBitRate](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature +- [sceVoiceUpdatePort](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature diff --git a/docs/dev/debt/libSceVoiceQoS.md b/docs/dev/debt/libSceVoiceQoS.md index a62a38f2d7..fa6732187f 100644 --- a/docs/dev/debt/libSceVoiceQoS.md +++ b/docs/dev/debt/libSceVoiceQoS.md @@ -1,10 +1,17 @@ # libSceVoiceQoS technical debt +### Silent stubs + +- [sceVoiceQoSReadPacket](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - network voice packet streaming is not implemented: it returns 0 and writes size 0 (indicating no packet is available). Aliens: Fireteam Elite (PPSA03393) calls it on its startup path (`0x76D14C`-`0x76D16E`) and stops with `not implemented` if it throws +- [sceVoiceQoSWritePacket](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - network voice packet streaming is not implemented: it returns 0 leaving `*size` untouched (treating all input bytes as accepted). Aliens: Fireteam Elite (PPSA03393) sets `*size` to the packet size before `WritePacket` (`0x76CEB9`-`0x76CEC4`), compares `*size` against that input size upon return (`0x76CF07`-`0x76CF10`) to verify all bytes were accepted, and stops with `not implemented` if it throws + ### Unknown function info - [sceVoiceQoSSetConnectionAttribute](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS, NID `KCGOxg8iX7s`) is a throwing stub declared without parameters, like the other endpoint calls of the library: PPSA04159 fails to load without it and its signature is unknown. The name hashes to the imported NID (`tools/nid_names.py`) -- [sceVoiceQoSInit](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - `0x804E0905` for a second initialization comes from [prosper](https://github.com/mattias800/prosper)'s reading of the module and was not measured: on a PS5 a valid call blocks inside a homebrew payload, so it never returns there. PPSA26344's two call sites (`0xE307BF`, `0x151C45D`) pass an allocated pointer, a 0x40000-byte block and app type 0x10000000; the app type and block are not used. `sceVoiceQoSCreateLocalEndpoint` takes four arguments (`int* id, userId, deviceInId, deviceOutId`) as declared in fpPS4 (`src/ps4_libscevoice.pas`, `ps4_sceVoiceQoSCreateLocalEndpoint`); no public reference exists for the other seven signatures (`DeleteLocalEndpoint(id)`, `CreateRemoteEndpoint(int* id)`, `DeleteRemoteEndpoint(id)`, `Connect(int* id, local, remote)`, `Disconnect(connection)`, `SetLocalEndpointAttribute(id, attribute, value, size)`, `SetRemoteEndpointAttribute(id, attribute, value, size)`), which are assumed from function naming and call patterns and not confirmed on a PS5 title. Endpoints and connections are tracked by allocated id; duplicate connections and deleting an endpoint while referenced in a connection are permitted as the console is not known to reject them; endpoint attributes are stored as given (including empty or non-positive sizes), as their ids and defaults are unknown; other endpoint and packet calls remain throwing stubs. The not-initialized error 0x804E0901 is assumed from the 0x804E09xx numbering, and unknown ids return `ARGUMENT_INVALID`. Aliens: Fireteam Elite (PPSA03393) uses all seven implemented calls on its first load path, starting with `sceVoiceQoSCreateLocalEndpoint` when it registers the local talker; it stops with `not implemented` when any of them throws +- [sceVoiceQoSInit](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - `0x804E0905` for a second initialization comes from [prosper](https://github.com/mattias800/prosper)'s reading of the module and was not measured: on a PS5 a valid call blocks inside a homebrew payload, so it never returns there. PPSA26344's two call sites (`0xE307BF`, `0x151C45D`) pass an allocated pointer, a 0x40000-byte block and app type 0x10000000; the app type and block are not used. `sceVoiceQoSCreateLocalEndpoint` takes four arguments (`int* id, userId, deviceInId, deviceOutId`) as declared in fpPS4 (`src/ps4_libscevoice.pas`, `ps4_sceVoiceQoSCreateLocalEndpoint`); signatures for `DeleteLocalEndpoint(id)`, `CreateRemoteEndpoint(int* id)`, `DeleteRemoteEndpoint(id)`, `Connect(int* id, local, remote)`, `Disconnect(connection)`, `SetLocalEndpointAttribute(id, attribute, value, size)`, `SetRemoteEndpointAttribute(id, attribute, value, size)`, and `GetLocalEndpointAttribute(id, attribute, value, size)` were verified against call sites in Aliens: Fireteam Elite (PPSA03393, e.g. `0x76D132`, `0x76D16E`, `0x76CECB`). Endpoints and connections are tracked by allocated id; duplicate connections and deleting an endpoint while referenced in a connection are permitted as the console is not known to reject them; endpoint attributes are stored as given (including empty or non-positive sizes), as their ids and defaults are unknown. The not-initialized error 0x804E0901 is assumed from the 0x804E09xx numbering, and unknown ids return `ARGUMENT_INVALID`. Aliens: Fireteam Elite (PPSA03393) uses all implemented calls on its load path; it stops with `not implemented` when any unimplemented call throws +- [libSceVoiceQoS firmware exports](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - `sceVoiceQoSGetRemoteEndpointAttribute`, `sceVoiceQoSGetConnectionAttribute` and `sceVoiceQoSGetStatus` follow public PS4 SDK prototypes in [Orbital](https://github.com/AlexAltea/orbital/blob/b321fcdbd700cc02da1cb1ad25412781d5ec73c6/tools/ida/db_types.json#L2436-L2441) and remain throwing stubs as endpoint and connection attribute storage is unverified on hardware. `sceVoiceQoSEnableChat`, `sceVoiceQoSDisableChat`, `sceVoiceQoSDebugGetStatus`, `sceVoiceQoSInitHQ`, `sceVoiceQoSSetMode`, and `sceVoiceQoSTerminate` are declared without parameters from the firmware export database (aerolib.csv); their guest signatures are unverified on hardware and every call throws ### Functional - [sceVoiceQoSEnd](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - `0x804E0901` (not initialized) before any initialization was measured on a PS5 (firmware 13.60, #1407); ending an initialized library was not, because a valid `sceVoiceQoSInit` never returns inside a homebrew payload. PPSA14632 calls it at shutdown and PPSA26344 after a nonzero `sceVoiceQoSInit`. After an initialization it returns 0 and clears the init flag, so the library can be initialized again. Whether the console also ends the libSceVoice instance that its `sceVoiceQoSInit` starts ([prosper](https://github.com/mattias800/prosper)'s reading of the module), and what it answers while endpoints exist, is unknown; here it drops every endpoint and connection +- [sceVoiceQoSGetLocalEndpoint, sceVoiceQoSGetRemoteEndpoint](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - signatures follow [Orbital](https://github.com/AlexAltea/orbital/blob/b321fcdbd700cc02da1cb1ad25412781d5ec73c6/tools/ida/db_types.json#L2437-L2439) (`GetLocalEndpoint(connectionId, int* localId)` and `GetRemoteEndpoint(connectionId, int* remoteId)`). Operations look up the connection by allocated id and return the connected local or remote endpoint id; an unknown connection id or null output pointer returns `ARGUMENT_INVALID` (0x804E0902), and calling before initialization returns `0x804E0901` diff --git a/docs/dev/debt/libSceVrSetupDialog.md b/docs/dev/debt/libSceVrSetupDialog.md index 34bc4cd532..336e3361d1 100644 --- a/docs/dev/debt/libSceVrSetupDialog.md +++ b/docs/dev/debt/libSceVrSetupDialog.md @@ -2,4 +2,4 @@ ### Silent stubs -- [libSceVrSetupDialog](../../../core/libs/prx/libSceVrSetupDialog/Export.cpp) - the VR setup dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled, as no headset is supported (see libSceHmd2). The signatures are assumed from the other common dialogs; PPSA12544 imports it +- [libSceVrSetupDialog](../../../core/libs/prx/libSceVrSetupDialog/Export.cpp) - the VR setup dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled, as no headset is supported (see libSceHmd2). The signatures are assumed from the other common dialogs; PPSA12544 imports the library. `sceVrSetupDialogGetStatus` (NID `qRUACYyUNFA`) is present in the firmware export database (aerolib), but direct import by PPSA12544 has not been explicitly confirmed. diff --git a/docs/dev/debt/libSceVrTracker2.md b/docs/dev/debt/libSceVrTracker2.md new file mode 100644 index 0000000000..42938b9e61 --- /dev/null +++ b/docs/dev/debt/libSceVrTracker2.md @@ -0,0 +1,11 @@ +# libSceVrTracker2 technical debt + +### Silent stubs + +- [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) never initializes, so its exports answer as the console's module does before initialization, measured on a retail PS5 (firmware 13.60) with no PS VR2 headset by calling each of the 115 NIDs with null arguments, zeroed buffers and buffers filled with 0xAA and 0x01: 99 return 0x8A740001 and write nothing; `XknY0IDcOwU` and `ark13GkiskA` return 0x8A740006 for a null first argument and otherwise clear its first 24 and 16 bytes and return 0x8A740001; `-YvjOUph8B0` returns 0x8A740006 for a null second argument and otherwise 0x8A740001; `QyZvFyf-QEk` and `fTYZOZepkqE` return 0 and write nothing; `TZIUIk9id3c` writes 0xC000, 0, 0x40 and 0x40 as `uint32`, `uint32`, `uint16` and `uint16` at offsets 4 to 15 of its first argument and returns 0; `IB0qcDVu8vU` clears the `uint32` at its first argument and returns 0. The last two crash on the console for a null first argument and throw here. Signatures and what the tracker returns once initialized are unknown; the twelve exports with aerolib names keep them and the others carry this library's own names + +### Unknown function info + +- [sceVrTracker2QueryMemory](../../../core/libs/prx/libSceVrTracker2/Export.cpp) (libSceVrTracker2, NID `TwqZnaIjWv4`) - accepts only a parameter whose first `uint32` is 0x20 and whose bytes 4 to 31 are zero, found by searching sizes 8 to 0x400 and setting each field and byte; it then writes 32 bytes, `uint32` 0x20, 32 MiB and 64 KiB followed by zeros, and returns 0, and otherwise returns 0x8A740006 without writing, as it does for a null argument. The meaning of the parameter's reserved bytes and of the written fields is assumed from their values +- [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) - initialization (`6Jy73SRfG-o`) takes one parameter of 0x48 bytes. PPSA05666 passes the size 0x48 at offset 0, `uint32` 0x100 and 0x2BC at 0x8 and 0xC, `uint64` 0x1FFF at 0x10 and 0x18, a pointer to its work memory at 0x20, and the memory query's 32 MiB and 64 KiB as `uint32` at 0x28 and 0x2C, with the other bytes zero; the priority and CPU mask meanings of 0x8 to 0x1F are assumed from the values. Called with that parameter and 32 MiB of anonymous memory from a homebrew payload on a PS5 (firmware 13.60) with no PS VR2 headset, it did not return within a minute, so what it returns without a headset is unknown and it throws +- [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) - eight exports throw. `6Jy73SRfG-o` (initialization) and `9R6W33Au4B4` return 0x8A740006 on the console for null arguments, for zeroed, 0xAA- and 0x01-filled buffers and for a size field from 8 to 0x400 in any of the first three arguments; initialization also returned it for the memory query's parameter with 32 MiB aligned to 64 KiB passed inside the parameter or as further arguments, so the parameter layout it accepts is unknown. `NBhWy+Gl0tY` returns 0x8A740006 for those buffers but crashes for null arguments; `jRmuXjsknsI` returns 0x8A740006 for null arguments and crashes for buffers, also when every 8-byte slot points at readable memory; `O8ZkTFtvUL4`, `940WPg+WwIk`, `oZDm1bynCaA` and `U1VYczwqukY` crash for both diff --git a/docs/dev/debt/libSceWebBrowserDialog.md b/docs/dev/debt/libSceWebBrowserDialog.md index bc07026490..3d44927711 100644 --- a/docs/dev/debt/libSceWebBrowserDialog.md +++ b/docs/dev/debt/libSceWebBrowserDialog.md @@ -3,3 +3,8 @@ ### Silent stubs - [libSceWebBrowserDialog](../../../core/libs/prx/libSceWebBrowserDialog/Export.cpp) - `sceWebBrowserDialogOpen` and `sceWebBrowserDialogOpenForPredeterminedContent` run the common-dialog state machine without displaying an interactive browser, and `sceWebBrowserDialogGetResult` reports that the user cancelled. Minecraft Bedrock (PPSA17221) imports `sceWebBrowserDialogOpenForPredeterminedContent` (#5744). The 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/libSceWkFontConfig.md b/docs/dev/debt/libSceWkFontConfig.md new file mode 100644 index 0000000000..20975e63cd --- /dev/null +++ b/docs/dev/debt/libSceWkFontConfig.md @@ -0,0 +1,5 @@ +# libSceWkFontConfig technical debt + +### Unknown function info + +- [libSceWkFontConfig pending exports](../../../core/libs/prx/libSceWkFontConfig/Export.cpp) - 5 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceWkFontConfig.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index ff94841f67..581f589299 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -2,22 +2,25 @@ ### Unknown function info -- [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. +- [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. On Windows, unwinding uses `_Unwind_Backtrace` and stops at a frame without `.ehmeta` or `.ehfram` tables whose PE unwind data has an exception or termination handler, leaving the trace empty or cut short while still returning 0. Implemented only on Linux and Windows; other platforms throw +- [ApplicationGuestHeapApi](../../../core/libs/prx/libc/src/ApplicationProcessParameters.cpp) (libc) - on Linux, HLE application allocation uses a loaded guest SDK's ten allocator exports when the title's replacement table has an allocator entry and the exports belong to one native module. With an empty table it keeps the default heap, since a bundled `libc.prx` may export the allocator NIDs while forwarding them to HLE. Its CRT owns application allocator initialization and finalization; HLE retains the module until process exit. Partial or mixed providers throw. This avoids reinitializing an allocator already used by SDK startup; console behavior and other host platforms remain unverified. - [fdopen](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) validates descriptor access modes as FreeBSD 11.4 [fdopen.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/fdopen.c) does: a requested mode incompatible with a descriptor not opened for both reading and writing returns null with `EINVAL`, leaving the descriptor open. Windows reads the handle's granted access through the same host helper as `fcntl(F_GETFL)`; a closed descriptor returns null with `EBADF`. Console behavior is unverified. - [scriptingGetMem, scriptingFreeMem](../../../core/libs/prx/libc/src/ApplicationHeap.cpp) (libc) - runtime allocator hooks for Unity titles. The allocation signature `(alignment, size)`, minimum alignment 16 and null result for a non-power-of-two alignment follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libKernel.cpp); the free hook's one-pointer signature follows [Raeen's recorded contract](https://github.com/Whoisraeen/Raeen/blob/2db56923204b89719916b95e29c2f3c4e1d9f90b/docs/unity-dlsym-blocker.md). Both use libc's own guest heap, not the title's registered allocator. A title that ships its own `libc.prx` (Metal Gear Solid Master Collection, PPSA18194) has already initialized that allocator, and reaching it from here would run its initializer a second time. That title's IL2CPP runtime looks up only `scriptingGetMem`, by name from `Il2cppUserAssemblies.prx`, and asks it for 256 KiB blocks aligned to 4 KiB. Which console library exports the hooks is unknown, and there is no console measurement. Allocation failure and sizes that overflow return null. - [_Mtx_trylock](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `k6pGNMwJB08`) - returns busy for a non-recursive mutex already owned by the caller, assumed to match `_Mtx_lock`'s ownership handling; exact console behavior is unverified. - [nearbyintf](../../../core/libs/prx/libc/src/Math.cpp) (libc, NID `c+4r-T-tEIc`) uses the host rounding mode, not the console's; console rounding mode state remains unverified. - [__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. - [fegetenv](../../../core/libs/prx/libc/src/Math.cpp) (libSceLibcInternal, implemented in libc, NID `psx0YiAKm7k`) follows FreeBSD 11 amd64 [fenv.c](https://github.com/freebsd/freebsd-src/blob/4554bee35405b36ea19e9e60d6abe6ea534e36e0/lib/msun/amd64/fenv.c) with the 32-byte `fenv_t` of [x86/fenv.h](https://github.com/freebsd/freebsd-src/blob/4554bee35405b36ea19e9e60d6abe6ea534e36e0/lib/msun/x86/fenv.h): `fnstenv` fills the 28-byte x87 part, MXCSR follows at 0x1c, and the x87 control word is reloaded because `fnstenv` masks every x87 exception. That the console's `libSceLibcInternal` has the same code and layout is assumed, not read from its binary. A null pointer throws instead of faulting - [Guest arena](../../../core/libs/prx/libc/src/GuestArena.cpp) (libc) - the application map area is assumed to end at 0xFC_0000_0000, as `SCE_KERNEL_APP_MAP_AREA_END_ADDR` does on the PS4. On Windows, host allocations made in the arena before libc loads stay, and a fixed guest mapping over one throws - [sceLibcInternalBacktraceForGame](../../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - `(const char* heapName)` and its return follow the `libc.prx` ASTRO BOT (PPSA21564 01.007.000) bundles: its four calls (0x6801, 0x7F46, 0xE75E, 0xEE02) are in the `malloc_stats` and `sceLibcMspaceMallocStats` paths, each after a `[%s] ...` line printed with the same heap name buffer, and the caller ignores the result and continues. The console's backtrace output is not reproduced: the heap name and the guest return address are written to the host standard error - [sceLibcInternalHeapErrorReportForGame](../../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - `(void* heap, void* block, uint32 error)` follows the same `libc.prx` (call at 0xF634): it passes the heap, the block and the nonzero result of its block check, and then calls a function that never returns, so the report only has to return. The meaning of the error values is unknown; the three values and the guest return address are written to the host standard error -- [sceLibcPafMspaceCreate, Destroy, Malloc, Free, Calloc, Realloc, Reallocalign, MallocStats and MallocStatsFast](../../../core/libs/prx/libc/src/Mspace.cpp) (libSceLibcInternal, implemented in libc) follow the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x5D680 to 0x5D770): each calls the same internal routine as its `sceLibcMspace` counterpart without the malloc callback hook those add, and `Reallocalign` takes the alignment before the size as `sceLibcMspaceReallocalign` does. `Create` takes a fifth argument, a pointer to a creation options block (a version word, a type of 1 or 2 at offset 2, a field at 0x10) that the `Mspace` form passes as null; a non-null block throws, as its meaning is unknown. `Malloc`, `Free`, `Calloc`, `Realloc` and `Reallocalign` take a trailing owner value that the console XORs with its block magic and stores in each block's footer, where the `Mspace` forms pass the heap itself. Footers are not modelled here, so the owner is ignored. Measured on a PS5 (firmware 13.60) from a homebrew payload, the calls with a matching owner behave as here, but the console ends the process when `Reallocalign` is given a block whose owner differs and when it resizes a block in place with an owner other than the heap, and `MallocStats` returns 1 instead of 0 while the heap holds blocks owned by something other than the heap; none of this is reproduced. `PafMspaceMemalign`, `PosixMemalign`, `MallocUsableSize`, `Trim`, `IsHeapEmpty`, `CheckMemoryBounds`, `ReportMemoryBlocks` and `GetFooterValue` are not implemented +- [sceLibcPafMspaceCreate, Destroy, Malloc, Memalign, PosixMemalign, Free, Calloc, Realloc, Reallocalign, MallocStats and MallocStatsFast](../../../core/libs/prx/libc/src/Mspace.cpp) - PAF exports delegate to `sceLibcMspace` without malloc hooks; `Memalign` and `PosixMemalign` forward to their `Mspace` counterparts with owner ignored and are unverified on console. `Create`'s options block (version, type 1 or 2 at +2, field at +0x10) throws when non-null as its meaning is unknown. Block footers are not modelled, so owners are ignored: on PS5 firmware 13.60 mismatched owners in `Reallocalign` or in-place `Realloc` kill the process and `MallocStats` returns 1 if non-heap-owned blocks exist, which is not reproduced. `MallocUsableSize`, `Trim`, `IsHeapEmpty`, `CheckMemoryBounds`, `ReportMemoryBlocks` and `GetFooterValue` are not implemented. - [malloc_usable_size](../../../core/libs/prx/libc/src/ApplicationHeap.cpp) (libc) - with the default allocator it returns the size the block was requested with (the range registered for it), not the rounded-up chunk size Sony's allocator reports; both satisfy "at least the requested size", and bytes past the request would not be write-watched. With an application allocator it calls entry 9 of the replacement table (`malloc_usable_size` in KytyPS5's `MallocReplace` order) and throws when the table leaves it null, like `malloc_stats_fast` - [malloc_stats_fast](../../../core/libs/prx/libc/src/GuestHeap.cpp) (libc) - with the default allocator it fills the 0x28-byte managed-size block as `sceLibcMspaceMallocStatsFast` does, from the default heap's own accounting, not Sony's allocator: the system sizes count the 1 MiB spans of its small-block classes and its committed large blocks, cached free ones included, and the in-use sizes count each handed-out block at its size-class or 64 KiB-granule size, header included. A block size below 0x28 or a null pointer returns 22, as in the mspace version; the version field is not checked. Ship of Harkinian (PPSA99320) calls it about 10 s after start-up. - [malloc, calloc, realloc, memalign, aligned_alloc and reallocalign](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - follow FreeBSD 11.4 `contrib/jemalloc/src/jemalloc.c` for failures, not measured on the console: a size the default heap rejects or the guest arena cannot place (including a `calloc` product that overflows) returns null with `errno` `ENOMEM`, and `memalign` and `aligned_alloc` return null with `EINVAL` for an alignment that is zero or not a power of two. FreeBSD's libc has no `memalign`, so setting `errno` there is assumed. A host commit failure for a block that fits in the arena (Windows `ERROR_NOT_ENOUGH_MEMORY`, `ERROR_OUTOFMEMORY` or `ERROR_COMMITMENT_LIMIT`, Linux `ENOMEM`) resets and releases the reserved range and returns null with `ENOMEM`; any other commit failure releases the range and throws. `reallocalign` returns null with `EINVAL` for an invalid alignment and `ENOMEM` like the others, which is an assumption as it is not in FreeBSD. `realloc(ptr, 0)` frees the block and returns null, where FreeBSD 11.4's jemalloc 4.5 resizes it to one byte and returns a block @@ -51,53 +54,68 @@ ### Functional -- [sscanf_s and vsscanf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) return `EOF` when a reached conversion has a null destination, stopping before further assignments, following C11 K.3.5.3.7 and K.3.5.3.14. Earlier assignments are retained. The constraint handler is not invoked, and console behaviour is unverified. +- [_FSinh and _FCosh](../../../core/libs/prx/libc/src/Math.cpp) (libc) finite results are not verified bit for bit against the console: the scaled product uses the host's binary64 hyperbolic function before rounding to binary32, rather than the float rational and `_FExp` sequence in PRAGMATA's bundled `libc.prx`. NaN preference and payloads, signed zeros, infinite operands and the range-error thresholds follow that binary's instructions; the system `libSceLibcInternal` implementation and nondefault flush-to-zero modes are unverified. +- [sscanf_s and vsscanf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) return `EOF` when a reached conversion has a null destination, stopping before further assignments, following C11 K.3.5.3.7 and K.3.5.3.14. Earlier assignments are retained. The constraint handler is not invoked, and console behaviour is unverified. +- [memmem and memccpy](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `memmem` searches for needle occurrences in memory buffers and `memccpy` copies buffers until a character match, following FreeBSD and POSIX specifications; behavior is unverified on console. + +- [Guest time conversions](../../../core/libs/prx/libc/src/Time.cpp) rewrite the host process `TZ` when a guest changes its own `TZ`, so other host consumers of that variable (including libSceRtc local time) shift too; code outside libc does not take the zone lock. Zone syntax follows the host C runtime, not FreeBSD: Windows only accepts the `std offset [dst]` form, treats an empty guest `TZ` as `UTC0` and reads `:Europe/Berlin` as an abbreviation (for example `rop`); only Linux honours zone file names. If the guest accepts a `TZ` value that the host rejects (on Windows, for example, one longer than the host environment limit), conversions throw `std::runtime_error`. A `tm_zone` saved before a `TZ` change can refer to storage now holding the new zone name, changing `%Z` output. - [__isfinitel](../../../core/libs/prx/libc/src/Math.cpp) - classifies the x87 80-bit exponent without narrowing to double or performing floating-point comparisons, following [FreeBSD msun](https://github.com/freebsd/freebsd-src/blob/main/lib/msun/src/s_isfinite.c). The signature and classification follow FreeBSD; console behaviour is unmeasured. Requires little-endian x87 long double stored in 16 bytes, as the existing guest long-double ABI does. - Windows direct-memory backing reserves a paging-file section. A [direct memory page](../../../core/libs/prx/libc/include/WindowsMappings.hpp) mapped at one guest address is private write-watched memory at that address and is not committed in the section; a page mapped at a second address moves its whole resident run into the section (committed there) and every address becomes a view of it, tracked by write protection. An unmapped resident page that is not all zeros is kept in a private reservation until it is mapped again. Committed section contents remain across unmap/remap and are released when the backing and its remaining views are destroyed. A host import alias over part of a resident run is refused. Linux retains its existing backing. Peak gameplay memory and Windows versions other than the tested host remain unverified. +- Stable Windows resident runs of at least 256 KiB use temporary read-only protection for clean write-watch proofs. The first CPU write restores the logical protection of the whole run; that run remains on native write watching until it is remapped, so repeatedly rewritten runs stay writable. Native kernel writes into guest buffers must hold [GuestArena::HostWrite](../../../core/libs/prx/libc/include/GuestArena.hpp), as shared views already require: an unbracketed WinAPI ReadFile can fail with ERROR_NOACCESS while the resident proof is armed. Guest read, pread and socket receive paths already bracket their destinations. GPU host-import access suppresses resident rearming without inventing shared-page stores. This is a host integration requirement, not a change to guest logical protection. - [getc_unlocked, putc_unlocked, getchar_unlocked, putchar_unlocked](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libc) - byte I/O uses the host unlocked operations; guest EOF/error flags and macro counters follow `FileStream`. Linux status queries use `feof_unlocked`/`ferror_unlocked` because [glibc `feof`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/feof.c) and [`ferror`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/ferror.c) can acquire the stream lock. Windows uses [the CRT no-lock byte operations](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/fgetc-nolock-fgetwc-nolock) and `feof`/`ferror`, verified under a held stream lock on UCRT. Console behavior and other host CRTs remain unverified. +- [feof_unlocked and ferror_unlocked](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libSceLibcInternal, implemented in libc) have no console measurement. The firmware 3.20 [export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibcInternal.c) names NIDs `NuydofHcR1w` and `yxbGzBQC5xA`. They follow FreeBSD 11 [feof.c](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/stdio/feof.c) and [ferror.c](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/stdio/ferror.c), returning 0 or 1 from the stream status without acquiring the stream lock. They inherit the host CRT limitations of the unlocked byte operations above. - [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 - [lgamma and signgam](../../../core/libs/prx/libc/src/Math.cpp) (libc) - `lgamma` returns the host's `lgamma` and stores the sign of Γ(x) in the exported `signgam` (NID `5gOOC0kzW0c`, starting at 0 as in FreeBSD 11's [`s_signgam.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/msun/src/s_signgam.c)) by the rules of [`e_lgamma_r.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/msun/src/e_lgamma_r.c): -1 for -0 and for a negative non-integer whose floor is odd, 1 otherwise, including poles, infinities and NaN. Taken from the FreeBSD source, not measured on the console. The NID is in the aerolib symbol list; no title is known to read `signgam`. PPSA99004 imports `lgamma` - [sinhf](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `sinhf` of the `libc.prx` bundled with PPSA28319 (disassembly): FreeBSD msun's [`e_sinhf.c`](https://github.com/freebsd/freebsd-src/blob/5aa554c7e5b6fd9042986df07f12ac5687e41392/lib/msun/src/e_sinhf.c) from before `__ldexp_expf`, which squares `expf(|x| / 2)` between `logf(FLT_MAX)` and the overflow threshold, calling msun's [`expm1f`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/s_expm1f.c) and [`expf`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/e_expf.c), whose paths for the arguments it passes are inlined here. Past the threshold Sony's error helper sets `errno` to `ERANGE` before returning `x * 1e37`; msun itself sets no `errno`. That code, run on a Windows host, gives the same bits as this implementation for every finite argument up to the threshold, of both signs; it was not run on Linux. That the system `libSceLibcInternal`, which Ruffle (PPSA68091) and RetroArch (PPSA99169) import while bundling a zero-return `libc.prx`, has the same code is assumed -- [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. 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 +- [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: - - a system call that writes into a protected page fails with `EFAULT` instead of faulting. `GuestArena::HostWrite` lifts the protection for the calls it brackets (file reads and socket receives); a host call that writes guest memory without it (`clock_gettime` and the like into a watched page) fails; + - a system call that writes into a protected page fails with `EFAULT` instead of faulting. `GuestArena::HostWrite` lifts the protection for the calls it brackets (file reads and socket receives), as [Host writes into guest memory](../CONVENTIONS.md#host-writes-into-guest-memory) requires; a host call that writes guest memory without it (`clock_gettime` and the like into a watched page) fails; - code that changes the protection of watched pages, or maps over them, has to call `GuestWriteWatchProtectionChanged`, and code that asks the kernel for a page's protection has to ask `GuestWriteWatchProtection` first, since a watched page is read-only between a collection and the next write. Nothing calls either yet: the macOS memory paths are not in the tree; - the handler is installed when libc loads and passes other faults to the handler installed before it. A handler installed later has to do the same. -- On macOS the [guest arena](../../../core/libs/prx/libc/src/GuestArena.cpp) finds host regions with `mach_vm_region` and reserves the rest with a fixed `mach_vm_map`, which fails instead of replacing, as macOS has no `/proc/self/maps` or `MAP_FIXED_NOREPLACE`. The kernel keeps 0x10_0000_0000-0x70_0000_0000 unmappable for every process (max protection none; macOS 26.5, M1 Pro, arm64 and x86_64 under Rosetta), and dyld and the Rosetta runtime sit near 0x2_0200_0000 and 0x3_0400_0000, so a guest mapping fixed inside those ranges fails where the console places it; mappings without a fixed address are placed around them. +- On macOS the [guest arena](../../../core/libs/prx/libc/src/GuestArena.cpp) finds host regions with `mach_vm_region` and reserves the rest with a fixed `mach_vm_map`, which fails instead of replacing, as macOS has no `/proc/self/maps` or `MAP_FIXED_NOREPLACE`. The kernel keeps the range up to 0x70_0000_0000 unmappable for every process (max protection none). It starts at 0x10_0000_0000 on macOS 26.5 (M1 Pro, arm64 and x86_64 under Rosetta) and at 0xF_C000_0000 on macOS 27 (27.0.1 on an M4 Pro, arm64 and x86_64 under Rosetta; 27.2 on an M2 Max, arm64). With dyld and the Rosetta runtime near 0x2_0200_0000 and 0x3_0400_0000, a guest mapping fixed inside those ranges fails where the console places it; mappings without a fixed address are placed around them. - [sceKernelSync](../../../core/libs/prx/libc/src/General.cpp) (libkernel) flushes only the host files and directories recorded through `RecordWrittenPath_nid_no_patch` (the libc, libkernel, save data and app content file functions) and their changed parent directories. A host write in a library that does not record its path is not flushed by `sceKernelSync` - [sceKernelSync](../../../core/libs/prx/libc/src/General.cpp) (libkernel) on Windows skips a recorded file it cannot open for writing (read-only, or held open by a handle that denies write sharing), since `FlushFileBuffers` needs a writable handle. On Linux, it skips a recorded file it cannot open for reading. Such a file's data may not reach the disk before a host crash; the guest reads the same data either way -- [`fopen` streams](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) on Windows share delete access, like the descriptors [`sceKernelOpen`](../../../core/libs/prx/libkernel/File/src/Open.cpp) opens, so a file the guest holds open can be removed or renamed. A stream reopened with `freopen` still goes through `_wfreopen`, which does not share delete access. On Windows, opening a directory in a read mode fails with `EACCES`, where FreeBSD opens it; directory streams are not emulated +- [`fopen` and `freopen`](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) on Windows fail to open a directory in a read mode with `EACCES`, where FreeBSD opens it; directory streams are not emulated +- [readdir and readdir_r](../../../core/libs/prx/libc/src/Directory.cpp) (libc) on Windows report, on a file system whose directory listing carries no file ID (the entry's ID is 0, or the volume refuses `FileIdBothDirectoryInfo`), a nonzero `d_fileno` hashed from the entry name and the directory's file index, which differs from `lstat`'s `st_ino`. Not reproduced: NTFS lists an ID for every entry +- [Writes on an update stream](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) on Windows go through the UCRT, which drops a write that directly follows a read without `fflush`, `fseek`, `fsetpos` or `rewind` unless the read reached end of file: `fwrite` returns 0, and `fputc` and `fprintf` report success, without the error indicator. FreeBSD 11.4's [`__swsetup`](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libc/stdio/wsetup.c#L59-L74) discards the read buffer and writes at the descriptor's offset, past a read-ahead of `st_blksize` bytes for a regular file, so where the console writes depends on its block size. Reads directly after writes follow FreeBSD's `__srefill` - [`fopen` of /dev/random and /dev/urandom](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) in a read mode returns a stream over the host null device whose `fread` fills the buffer with host OS entropy, as the libkernel [random device](../../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) descriptors do. Only the exact paths are recognised, the stream has no descriptor, `setvbuf`, `feof`, `ferror`, `clearerr`, `flockfile`, `ftrylockfile`, `funlockfile` and `fclose` work on it, `fflush` returns 0 as on a FreeBSD read-only stream, and every other stream function throws, as does opening the paths for writing. `freopen` onto these paths still fails with `ENOENT`. ProsperoTV (PPSA99003) reads its phone pairing code this way +- [RemoveTree](../../../core/libs/prx/libc/include/RemoveTree.hpp) (libc) removes a host tree itself on Windows instead of calling `std::filesystem::remove_all`: MinGW libstdc++ up to GCC 16 does not treat a reparse point as a link, so `remove_all` recurses through it and never terminates, and any tree that holds a symlink hangs. It builds the entry list from `GetFileAttributesW`, removes a reparse point as the link it is rather than its target, clears the read-only attribute and retries as `remove_all` does, and reports the first native error. The libstdc++ fix is in GCC 17 (commit 88856ad855ae); other hosts call `remove_all`. - The [`_Unwind_Context`](../../../core/libs/prx/libc/include/exceptions/Unwind.hpp) that `_Unwind_Backtrace` passes to its callback starts with a table laid out like LLVM libunwind's `AbstractUnwindCursor` virtual table, because homebrew built with the ps5-payload-dev SDK links libunwind statically and reads the frames with its own `_Unwind_GetIP` (`__unw_get_reg`), which calls through that table. Only register reads and writes (`validReg`, `getReg`, `setReg`, with `UNW_REG_IP` and `UNW_REG_SP`), `validFloatReg` (always false) and `isSignalFrame` are answered; the other slots throw. The layout follows libunwind's `UnwindCursor.hpp`, not the console's own `libSceLibcInternal`, which is not checked - The Windows libc [exception unwinder](../../../core/libs/prx/libc/src/exception/Unwind.cpp) steps through frames of modules without the project's `.ehmeta` and `.ehfram` tables (system DLLs, and WinLibs GCC 15.2's `libstdc++-6.dll` and `libwinpthread-1.dll`, whose `.eh_frame` sections are empty) with their PE unwind data through `RtlVirtualUnwind`, but does not run Windows exception or termination handlers: it stops at a frame whose unwind data has one, such as a C++ cleanup through `__gxx_personality_seh0` in `libstdc++-6.dll` or a system DLL function like `qsort` in `ucrtbase.dll` (Windows 10 22H2), and the exception is then reported as unhandled - The 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. +- [wctomb_s, wcrtomb_s, mbstowcs_s and mbsrtowcs_s](../../../core/libs/prx/libc/src/BoundsChecking.cpp) (libc) follow the console's `libc.prx`, but convert in the single-byte C locale of `wctomb`, `wcrtomb` and `mbrtowc` (`MB_CUR_MAX` 1): a character above 0xFF is `EILSEQ` in `wctomb_s` and `wcrtomb_s`, and each byte is one wide character in `mbstowcs_s` and `mbsrtowcs_s`. The locale of a title's `libc.prx` was not measured. The `wcrtomb_s` path where a character does not fit `smax` and the `EILSEQ` paths of `mbstowcs_s` and `mbsrtowcs_s` are not reached - [wcsrtombs_s](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) follows the console's `libc.prx`, but a runtime-constraint violation only returns its error: the default handler there writes the message and a newline to `stderr` before returning, which is not done here. Conversion uses the C locale of `wcrtomb` (`MB_CUR_MAX` 1), so the path where a multibyte character does not fit the remaining space is not reached - [c16rtomb, c32rtomb, mbrtoc16 and mbrtoc32](../../../core/libs/prx/libc/src/Multibyte.cpp) (libSceLibcInternal, implemented in libc) 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 - [The narrow printf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) converts 16-bit guest wide arguments to UTF-8 on both platforms, independently of the host locale. It does not use the guest's single-byte `wcrtomb`; the console's treatment of invalid UTF-16 and locale-specific encoding remains unverified. Linux retains host formatting for formats without wide arguments, `%m` or `%p`, so a NaN there still follows the host (glibc prints its sign, `-nan`, and honors the `+` and space flags, where FreeBSD prints `nan`); argument positions above 99 (FreeBSD's `NL_ARGMAX`) throw when a format requires the guest formatter - [%p and %lp](../../../core/libs/prx/libc/include/WindowsFormatting.hpp) (libc) follow the console's `libSceLibcInternal.sprx` measured from a homebrew payload on firmware 12.70 ([#7549](https://github.com/boykopovar/AnyPS5/issues/7549)), not FreeBSD: bare lowercase hex with no `0x` prefix even under `#`, `0` for a null pointer, `+` and the space flag ignored, the width counting only the digits, `-` padding on the right, `0` padding with zeros when there is no precision, the precision as the minimum number of digits, nothing for a null pointer under `%.0p`, and `l` accepted. Only `snprintf` and `swprintf` of that module were measured, from a payload process rather than a title process, so a width beside `%.0p`, the `0` flag beside a precision (which C17 ignores) and `%.0p` of a non-null pointer follow this formatter's own rules and are not measured; `swprintf` was not run with `%lp`, so the wide family still refuses that conversion and returns -1. Linux routes `%p` and `%lp` to this formatter because glibc prints the FreeBSD form and `(nil)` for a null pointer, which means a format that combines `%p` with a conversion this formatter refuses, such as an argument position above 99, throws there instead of reaching glibc - [collate and collate _Getcat](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) build the facet for the C locale only and throw for any other locale name, the wide entry point following the narrow one it sits next to. The wide facet carries its own vtable, whose `compare`, `transform` and `hash` take the guest's 16-bit characters, so the length of a range is a count of characters: `compare` orders by 16-bit value (the C locale, as Dinkumware's `char_traits::compare` does), `transform` copies the characters into the result and records size and capacity in 16-bit elements (inline capacity 7, heap capacity `length | 7`, `(capacity + 1) * 2` bytes including the terminator), following Dinkumware's [VC12 `basic_string`](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/xstring#L500-L531), and `hash` mixes one whole character per round, so wide text below 0x100 hashes like its narrow spelling and a character above 0xff hashes as one value instead of the two bytes it holds. The console's `libc.prx` exports both `_Getcat` (NIDs BSVJqITGCyI and 2wz4rthdiy8) with identical 0x19d-byte bodies, believed to build the same 0x20-byte facet; the console bodies themselves are unreadable (obfuscated dynsym, and the text of the relinked `libc.prx` is zeroed), so the wide entries follow the narrow algorithm and the C locale instead of the console's wide code, and no console run checks the two. The console's `libc.prx` stores pointers to its collation state tables at offsets 0x10 and 0x18 of the facet; ours leaves them null, as only the facet's own functions read them. `do_transform` throws for input with embedded null characters, which the console drops depending on the output buffer size. The narrow result capacity (`length | 15`) and the over-aligned storage of results of 4080 characters or more follow the console's `libc.prx` code and are not checked on hardware. No module of the PPSA59322, PPSA76677 or PPSA97358 dumps in this tree imports either `_Getcat`, so neither has been exercised by a title. - [std::__throw_bad_variant_access](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libc) throws `std::bad_variant_access` with the `what()` string `bad_variant_access`, the one [LLVM libc++](https://github.com/llvm/llvm-project/blob/main/libcxx/src/variant.cpp) returns, since the export carries libc++'s `__throw_bad_variant_access` name; the console's string is unverified. Onimusha: Way of the Sword (PPSA27836 01.000.001) imports it from `libc`, and its bundled `libc.prx` does not export it. -- [The scanf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc), the `_s` forms included, on Linux throws for the 16-bit wide conversions (`%ls`, `%lc`, `%l[`, `%S`, `%C`): glibc would store a 32-bit `wchar_t` per character into the guest's 16-bit buffer. Windows hands `%ls`, `%lc` and `%l[` to the host, whose `wchar_t` is 16 bits +- [std::bad_optional_access and std::_Throw_bad_optional_access](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libc) provide the exception type, constructor, destructor, and `what()` exports alongside `_Throw_bad_optional_access`, returning the `what()` string `bad_optional_access` matching Dinkumware and libc++ conventions; the console libc.prx string is unverified. PPSA26578 (issue #7624) and PPSA34015 import `_ZSt26_Throw_bad_optional_accessv` (`KwzEip7m6TM`) from `libc`, and fail to load on main with unresolved import (0xC0000139). +- [The scanf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc), the `_s` forms included, decodes the input of the 16-bit wide conversions (`%ls`, `%lc`, `%l[`, `%S`, `%C`) on Linux as UTF-8, mirroring the narrow printf family, where FreeBSD uses the locale's `mbrtowc` (one byte per character in the C locale); the console's locale encoding is unverified. Input that is not UTF-8, a character outside the BMP, a scanset member outside ASCII, positional arguments next to a wide conversion and wide conversions in `fscanf` throw. Each character is stored as one 16-bit unit, widths and `_s` sizes count characters, and a `%lc` field cut short by the end of the input keeps what it read, as FreeBSD's `convert_wchar` does. Windows hands `%ls`, `%lc` and `%l[` to the host, whose `wchar_t` is 16 bits - [The scanf family](../../../core/libs/prx/libc/include/WindowsScanning.hpp) (libc) on Windows returns 0, as C17 and FreeBSD do, when input fails after only suppressed conversions have completed (`sscanf("12", "%*d%d", &value)`). Linux hands the format to glibc, which returns `EOF` there. On Windows the host stops a `%x`, `%X`, `%i` or `%p` after the `x` of a `0x` that no hex digit follows (or that the width cuts off), and a float conversion (`%e`, `%f`, `%g`, `%a`) after the `e` of an exponent with no digits (`1e`), where C17 and FreeBSD stop before the `x` or the `e` -- [Guest path resolution](../../../core/libs/prx/libc/src/General.cpp) falls back to ASCII case-insensitive component lookup, following [shadPS4's host filesystem](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/file_sys/backends/host_fs.cpp). PPSA03747 requests `Skyrim.INI`, `Data/Video` and lowercase archive/plugin names that differ from the dump's filenames. Exact host names take precedence; multiple fallback matches throw instead of selecting an arbitrary file. Existing parent names are resolved when creating a new file, and guest mount aliases use the same ASCII comparison. Results are not cached, so renames and newly created files remain visible. Unicode case folding and PS5 behaviour for case-colliding host entries are unverified; host roots are used as supplied. Explicit native Windows drive and UNC paths bypass guest parent-component validation.. A relinked module's `.guest.prx` is matched the same way when the original module is not beside it: Borderlands 4 (PPSA01494) loads `/app0/prx/libRenoirCore.PS5.prx`, which it ships as `librenoircore.ps5.prx` +- [Guest path resolution](../../../core/libs/prx/libc/src/General.cpp) falls back to ASCII case-insensitive component lookup, following [shadPS4's host filesystem](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/file_sys/backends/host_fs.cpp). PPSA03747 requests `Skyrim.INI`, `Data/Video` and lowercase archive/plugin names that differ from the dump's filenames. Exact host names take precedence; multiple fallback matches throw instead of selecting an arbitrary file. Existing parent names are resolved when creating a new file, and guest mount aliases use the same ASCII comparison. Results are not cached, so renames and newly created files remain visible. Unicode case folding and PS5 behaviour for case-colliding host entries are unverified; host roots are used as supplied. Explicit native Windows drive and UNC paths bypass guest parent-component validation.. A relinked module's `.guest.prx` is matched the same way when the original module is not beside it: Borderlands 4 (PPSA01494) loads `/app0/prx/libRenoirCore.PS5.prx`, which it ships as `librenoircore.ps5.prx`. A relinked module request that misses both the exact and the case-insensitive `.guest.prx` takes the sibling whose stem (the name without its last extension, ASCII case ignored) equals the requested stem and that ends with `.guest.prx`, shortest name first: Unity IL2CPP titles load `Media/Modules/.prx` and `Media/Plugins/.prx`, and the relinker names the converted module after the ELF it was built from, `.elf.guest.prx`. The console's module namespace is assumed to ignore case and the extension; several stem matches pick the shortest name instead of throwing, which is not measured on a console - [Colons in guest names](../../../core/libs/prx/libc/src/General.cpp) (libc) - on Windows (NTFS) a guest name containing `:` is not a plain file name: `name:stream` opens or creates the alternate data stream `stream` of the file `name`. Creating `/savedata0/C:file` makes an empty file `C` whose stream `file` holds the data, and `stat` of `/savedata0/C` then reports a regular file. `mkdir` of a name containing `:`, such as `C:` or `dir:x`, fails with EINVAL. `name:stream` already behaved this way before drive-letter components were kept under their mount - [tzname](../../../core/libs/prx/libc/src/Time.cpp) (libc) points at the host's zone names, read at load and again by `tzset`: the abbreviations a `TZ` string gives (`EST5EDT` is `EST` and `EDT` on both hosts), but on Windows without `TZ` the CRT's full names such as `GMT Standard Time`, which ICU's [uprv_tzname](https://github.com/unicode-org/icu/blob/457157a92aa053e632cc7fcfd0e12f8a943b2d11/icu4c/source/common/putil.cpp#L1244-L1264) (ES-DE in PPSA99169) cannot map to a zone ID. A zone with no daylight-saving time has `tzname[1]` empty on Windows and equal to `tzname[0]` on Linux, where FreeBSD leaves three spaces. The console's names are not known - [Trailing slash on a mount alias](../../../core/libs/prx/libc/src/General.cpp) (libc) - on Windows a trailing slash after a file name under a mount alias such as `/savedata0` is still dropped, so `unlink`, `rename`, `open` and the other path calls act on the file where FreeBSD 11.4's `lookup()` fails with `ENOTDIR`; Linux and macOS pass the slash to the host, as for paths outside an alias @@ -105,11 +123,14 @@ - [strftime and asctime](../../../core/libs/prx/libc/src/Time.cpp) (libc) follow FreeBSD 11 [strftime.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libc/stdtime/strftime.c) and [asctime.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/contrib/tzcode/stdtime/asctime.c) with the C locale on every host, not measured on the console; other locales are not implemented. `%s` follows FreeBSD 9 and 11 and is the local-time `mktime` of the broken-down time (current FreeBSD uses `tm_gmtoff`), which is -1 on Windows outside the range above. `%Z` is `tm_zone`, or `tzname[tm_isdst != 0]` when it is null and `tm_isdst` is not negative. The two-digit year warning FreeBSD writes to `stderr` when `CHECK_STRFTIME_FORMATS_FOR_TWO_DIGIT_YEARS` is set is not written - [wcsftime](../../../core/libs/prx/libc/src/Time.cpp) (libc) follows FreeBSD 11.4's [wcsftime.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/locale/wcsftime.c) over this library's `wcsrtombs`, `strftime` and `mbsrtowcs` in the single-byte C locale of [none.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/locale/none.c), with the guest's 16-bit `wchar_t`, without console verification. It has the `strftime` gaps above. A format unit above 255 returns 0 with `EILSEQ`; an empty result, a result that does not fit and a null buffer return 0 with `errno` unchanged, discarding the `ERANGE` or `EINVAL` that the Windows CRT's `strftime` sets. A null format or `tm` throws, where FreeBSD dereferences it (a null `tm` is read there only when the format has a conversion that needs it) - [syslog](../../../core/libs/prx/libc/src/Syslog.cpp) (libc) writes each message to `stderr` as `[syslog:] `, with a newline added when missing, instead of sending it to the console's system log. Priority and facility handling follow FreeBSD's `vsyslog`: `%m` expands to the guest errno's message, unknown bits are reported (as priority 0x23, since FreeBSD passes `LOG_ERR|LOG_CONS|LOG_PERROR|LOG_PID` as the priority) and masked, a missing facility becomes `LOG_USER` or the facility set by `openlog`, and errno is preserved. `openlog` applies its identity and `LOG_PID`; a null identity retains the previous one, and a valid nonzero facility replaces the default. `closelog` clears the identity but retains the options and facility, as in FreeBSD 11; when no identity is set, FreeBSD falls back to the program name, which this backend does not emit. `LOG_PERROR` has no extra effect because stderr is the emulated log sink; `LOG_NDELAY`/`LOG_ODELAY`, `LOG_NOWAIT` and `LOG_CONS` have no daemon, child or console backend to control. `setlogmask` follows [FreeBSD 11](https://github.com/freebsd/freebsd-src/blob/8d2708de5d6112a492744d367cfdfb8fd219227a/lib/libc/gen/syslog.c): the process-wide mask defaults to 0xff, zero queries it, and nonzero values replace it; masked messages are skipped before formatting, including the unknown-priority diagnostic. The console's mask behaviour is unverified +- [fwscanf, vfwscanf, wscanf, vwscanf and their `_s` forms](../../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) run the `swscanf` scanner on characters read with `fgetwc`, so everything listed for `swscanf` in this file applies to them. As in the console's `libc.prx`, a field takes characters while they can still begin a valid item, so a failed field such as `0x` for `%x` stays consumed, and only the character that ended the scan goes back to the stream. That character is pushed back as its UTF-8 bytes with `ungetc`, because `ungetwc` takes single bytes only; the Windows C runtime guarantees one byte of push-back, so a non-ASCII lookahead can be lost there. A null stream or format returns `EOF`; the console checks them only in the `_s` forms. - [daemon](../../../core/libs/prx/libc/src/Process.cpp) (libc) throws: forking into the background is not supported. PPSA85300 imports it and fails to load without it. VLC 3.0 calls it only when its `daemon` option is set ([libvlc.c](https://github.com/videolan/vlc/blob/6de05adcbaf2e8b85fe86aad4169393098628119/src/libvlc.c#L190-L197)), and the title starts libVLC with `--ignore-config` and without `--daemon` ([player.cc](https://github.com/ZiZc3/VLC/blob/f3253020a64368153233ede29183014c0b6c8979/src/player.cc#L960-L981)) -- [_WStoul](../../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../../core/libs/prx/libc/src/RuneLocale.cpp), [swscanf_s and swscanf](../../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) - `_WStoul` is `wcstoul` and throws for a base other than 0 or 2 to 36. `_Iswctype` takes the class numbers of Dinkumware's [xwcc.h](https://github.com/superna9999/ti-cgt6x/blob/cgt6x_7_3_4/include/xwcc.h) (1 `alnum` to 11 `xdigit`, 12 `blank`) and throws for any other value, including the FreeBSD masks `wctype` returns; the numbering is not confirmed on the console's `libc.prx`. `swscanf_s` follows C11 7.29.2.2 and K.3.9.1.5, but reads the size argument as `unsigned int` like `sscanf_s` where K.3.9.1.5 specifies `rsize_t`; the console's width is unverified. A runtime-constraint violation (null buffer, format or receiving pointer) only returns `EOF`, and a too small array gets a null first element as in `sscanf_s`. A literal mismatch is a matching failure and a field that is only a prefix, such as `0x` or `1e`, does not convert, as C11 requires; FreeBSD's `vfwscanf` reports an input failure for the first and converts the second. Integers out of range are truncated from `strtoll`/`strtoull`. It throws for `%p`, `%S`, `%C`, unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert. `swscanf` runs the same scanner without the size arguments of `%s`, `%c` and `%[`, as FreeBSD's [swscanf.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libc/stdio/swscanf.c) passes its arguments to `vswscanf`, so everything above except the sizes applies to it; it also keeps the null buffer, format and receiving pointer checks returning `EOF`, where C leaves those undefined. Neither variant is confirmed on a PS5 title +- [_WStoul](../../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../../core/libs/prx/libc/src/RuneLocale.cpp), [swscanf_s and swscanf](../../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) - `_WStoul` is `wcstoul` and throws for a base other than 0 or 2 to 36. `_Iswctype` takes the class numbers of Dinkumware's [xwcc.h](https://github.com/superna9999/ti-cgt6x/blob/cgt6x_7_3_4/include/xwcc.h) (1 `alnum` to 11 `xdigit`, 12 `blank`) and throws for any other value, including the FreeBSD masks `wctype` returns; the numbering is not confirmed on the console's `libc.prx`. `swscanf_s` follows C11 7.29.2.2 and K.3.9.1.5, but reads the size argument as `unsigned int` like `sscanf_s` where K.3.9.1.5 specifies `rsize_t`; the console's width is unverified. A runtime-constraint violation (null buffer, format or receiving pointer) only returns `EOF`, and a too small array gets a null first element as in `sscanf_s`. A literal mismatch is a matching failure and a field that is only a prefix, such as `0x` or `1e`, does not convert, as C11 requires; FreeBSD's `vfwscanf` reports an input failure for the first and converts the second. Integers out of range are truncated from `strtoll`/`strtoull`. `%p` follows FreeBSD 11's [vfwscanf.c](https://github.com/freebsd/freebsd-src/blob/4ab2e064d7950be84256d671a7ae93f87cc6aa36/lib/libc/stdio/vfwscanf.c#L610-L615): a hexadecimal integer with an optional `0x`, converted as unsigned and stored as a pointer; a length modifier on it throws, where FreeBSD ignores it. It throws for `%S`, `%C`, unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert. `swscanf` runs the same scanner without the size arguments of `%s`, `%c` and `%[`, as FreeBSD's [swscanf.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libc/stdio/swscanf.c) passes its arguments to `vswscanf`, so everything above except the sizes applies to it; it also keeps the null buffer, format and receiving pointer checks returning `EOF`, where C leaves those undefined. Neither variant is confirmed on a PS5 title - [flock](../../../core/libs/prx/libc/src/FileLock.cpp) (libkernel) on Windows shares one lock state between duplicated descriptors, following FreeBSD 11.4's [flock(2)](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/sys/flock.2). Independent opens have separate states; closing a descriptor or reopening a stream drops only its reference. Lock transitions on one open file description are serialized. The lock is a host [LockFileEx](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-lockfileex) lock on one byte at offset 2^63 - 2, which no file reaches, so it excludes other `flock` calls and leaves reads, writes and mapped views alone, like FreeBSD's advisory locks. Console behavior, signal interruption and concurrent closure of a descriptor while its lock call is pending are unverified. -- [std::_Throw_Cpp_error](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libSceAmpr) follows Dinkumware's `thread0.cpp` as shipped with [Visual C++ 2013](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/thread0.cpp) and in [microsoft/STL](https://github.com/microsoft/STL/blob/da52dc0fcadbd83690cfd1ed32408d2847439493/stl/src/thread0.cpp), without console verification. `_Cpp_error` codes 0 to 6 throw a guest `std::system_error` whose `error_code` holds the FreeBSD errno of the `errc` value (EBUSY 16, EINVAL 22, ESRCH 3, ENOMEM 12, EPERM 1, EDEADLK 11, EAGAIN 35) and the generic category, and whose `what()` is the table message, `": "` and the generic category's message for that errno, selected by the first matching errno entry in VC12's [`syserror.cpp`](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/syserror.cpp); another code throws `std::out_of_range` on the host instead of reading past the table. The guest `system_error` is taken to be [VC12's](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/system_error): the 16-byte `runtime_error` followed by `error_code {int, const error_category*}`, 32 bytes, so its copy constructor and assignment copy the code too. The generic category is a static libc object with VC12's `error_category` virtual order (destructors, `name`, `message`, `default_error_condition`, the two `equivalent`), not the console's `_Error_objects::_Generic_object`, and the new `generic_category()` export returns that object; a title's inline category object in a bundled libc can still have a different identity. Its `message` and direct message export construct the existing [GuestLocale::String](../../../core/libs/prx/libc/include/GuestLocale.hpp) model: a reserved eight-byte word, 16-byte inline buffer, length and capacity, with guest `operator new` storage above 15 bytes. This 40-byte string layout and virtual return ABI have not been verified against a console library. The deleting destructor still throws. `_Syserror_map` covers the 72 distinct Dinkumware `errc` values present in FreeBSD 11.4 [errno.h](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/sys/errno.h), returning null for other values; category messages use `unknown error` for those. First-match aliases are `EAGAIN`/`EWOULDBLOCK` -> `operation would block` and `ENOTSUP`/`EOPNOTSUPP` -> `not supported`. STREAMS-only errno assignments absent from that header (`ENODATA`, `ENOSR`, `ENOSTR`, `ETIME`) are not inferred -- [Call tracing](../../../core/libs/prx/libc/src/specifics/windows/CallTrace.cpp) (`--trace-imports`, `APS5_TRACE_CALLS`) exists on Windows only and records the entry of a call, not its arguments or its result. One process-wide ring keeps the last 65536 calls, so a thread that calls often pushes the others out. Calls are also counted per traced target since start-up; an import bound to a target already traced under another name is counted under the first name. The thunk saves the integer argument registers and `xmm0` to `xmm7`; arguments in wider vector registers pass only as long as the recording code stays free of AVX instructions, which is not tested +- [std::_Throw_Cpp_error](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libc) follows Dinkumware `thread0.cpp` ([VC12](https://github.com/ganboing/crt.vc12/blob/f1d47126/src/thread0.cpp), [STL](https://github.com/microsoft/STL/blob/da52dc0f/stl/src/thread0.cpp)), without console verification. Codes outside 0..6 throw `std::out_of_range`, the deleting destructor throws, and STREAMS errnos absent from FreeBSD 11.4 [`errno.h`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/sys/errno.h) (`ENODATA`, `ENOSR`, `ENOSTR`, `ETIME`) are omitted. Codes 0..6 throw a 32-byte [VC12 `system_error`](https://github.com/ganboing/crt.vc12/blob/f1d47126/src/system_error) with FreeBSD errnos (16, 22, 3, 12, 1, 11, 35), a static `generic_category` (not `_Generic_object`), and unverified 40-byte [GuestLocale::String](../../../core/libs/prx/libc/include/GuestLocale.hpp) messages from [`syserror.cpp`](https://github.com/ganboing/crt.vc12/blob/f1d47126/src/syserror.cpp) +- [Call tracing](../../../core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp) (`--trace-imports`, `APS5_TRACE_CALLS`) is missing on macOS and records only the entry of a call, not its arguments or result. One process-wide ring keeps the last 65536 calls, so a busy thread pushes the others out; the reporting thread's own calls are listed only while they are still in that ring. Calls are counted per traced target; an import bound to a target already traced under another name is counted under the first name. The thunk saves the integer argument registers and `xmm0` to `xmm7`; wider vector arguments survive only while the recording code avoids AVX, which is untested. Linux has no crash report, so it prints the trace only on `SIGABRT` (guest or host `abort()`), not on `SIGSEGV`; a caller in a module loaded after the last traced import prints without a module name, and a thread aborting while another writes the report waits for it at most 2 seconds - [_Dtest](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows Dinkumware double classification matching _FDtest: finite values return -1, subnormals -2, zeros 0, infinities 1, and NaNs 2. - [app0.zar](../../../core/libs/prx/libc/src/AppArchive.cpp) (libc) - a [ZArchive](https://github.com/Exzap/ZArchive) beside `app0/` is a read-only layer under it: a path missing from `app0/` is looked up in the archive, case-insensitively. It is not a console format; titles ship their files in a PFS image instead. Resolving an archive-only path through any call other than the archive-aware libkernel ones throws, so `fopen`, `stat`, `access`, `opendir` and the rest do not read the archive yet, and `opendir` of a directory that exists in both places, `app0/` itself included, and `fdopen` of an archived descriptor throw instead of missing its entries or reading the null device. A damaged compressed block throws instead of returning `EIO` - [fopen_s](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - returns the errno of the failed open, as C11 K.3.5.2.1 leaves the nonzero value open and Microsoft's CRT returns errno; the console's Dinkumware `fopen_s` is not verified +- [ios_base standard streams](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) `_Addstd` queues the stream in the standard stream table of Dinkumware [ios.cpp](https://github.com/ganboing/crt.vc12/blob/master/src/ios.cpp) (`stdstr`, `stdopens`, `_Addstd`, `_Ios_base_dtor`): it takes the first of slots 1 to 7 that is free or already holds the stream, and slot 8 (`NSTDSTR`) when none is, so slot 8 is shared by every stream past the first seven and registration never fails. It stores the 1-based slot index in the guest `IosBase::standardStream` (Dinkumware's `_Stdstr`, where 0 means not a standard stream) and counts the stream objects registered per slot; registering the same stream again reuses its slot and raises its count. Measured on a PS5 (firmware 13.60) from a homebrew payload calling the system `libSceLibcInternal` export: in a process whose own standard streams already hold slots 1 to 7, twelve distinct streams and a repeated one all get 8, written as a 64-bit value at offset 0x8 with no other byte of the object touched; the assignment of slots 1 to 7 to fresh streams is not observable there and follows VC12. The counts are `int`, not VC12's `char`, and the registration and the destructor lock the table where VC12 locks only `_Addstd`; a slot index past the table throws in the destructor instead of indexing out of bounds. The destructor decrements its slot's count and suppresses the rest of the destruction while the count stays positive, as `_Ios_base_dtor` does, and the slots of destroyed streams keep their pointer and stay reserved for that stream, as `stdstr` does. The streams themselves are destroyed by the title's own code as it registers them through `__cxa_atexit`; when the last reference closes, the unsupported stream storage and callback cleanup is still the throw it was and the locale is released as for a non-standard stream. +- [std::_Pad, _Thrd_join, _Thrd_detach, _Thrd_id](../../../core/libs/prx/libc/src/Threads.cpp) (libc) follow Dinkumware V6.00 `thr/xthread` and `thr/threadcall.cpp` ([crt.vc12 `f1d47126`](https://github.com/ganboing/crt.vc12/tree/f1d47126/src/thr)), without console verification. `_Thrd_id` throws because whether the console returns `pthread_t` or a kernel thread ID and at what width is unknown; `_Launch` omits `_Cnd_do_broadcast_at_thread_exit` and calls `pthread_create` registered by libkernel with default attributes. The 32-byte pad layout (`_Go()` vtable at 0x0, `_Cnd_t` at 0x8, `_Mtx_t` at 0x10, `bool _Started` at 0x18) is assumed from VC12, unverified. Failing primitives or thread start throw the guest `std::system_error`; null arguments throw. `_Thrd_join` returns `_Thrd_error` (4) on `pthread_join` failure and truncates the exit code to `int`; `_Thrd_detach` returns `_Thrd_error` (4) on `pthread_detach` failure and throws when none is registered diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 20db1591ed..7028b833ac 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)) @@ -13,14 +13,16 @@ ### Unknown function info - [sceKernelError](../../../core/libs/prx/libkernel/System/src/Error.cpp) follows [shadPS4's kernel implementation](https://github.com/shadps4-emu/shadPS4/blob/23c41333822fe8eca4c609b43119180af63ae4ac/src/core/libraries/kernel/kernel.cpp): zero returns zero; other guest POSIX error values receive the `0x80020000` bias without modifying errno. Arithmetic is defined modulo 32 bits to avoid signed overflow for inputs outside the errno domain. Console behavior for those inputs and PS5 equivalence have not been measured. +- [physhm_open, physhm_unlink](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel, NIDs `n371J5cP+uo` and `AUqJNkobQ1c`) - declared as throwing stubs: the PS5 physical shared memory API is not documented, and [shadPS4's module generator](https://github.com/shadps4-emu/moduleGenerator/blob/main/libraries/kernel.cpp) also declares both with no arguments and returns `ORBIS_OK`, so the signature, the handle a success would return and the console's behaviour are unmeasured. The no-argument `int` signature follows shadPS4. Forever Launcher (PPSA99030) imports both and stops at load on `physhm_open`, and the relinker resolves no call site for either in that title, so no handle is fabricated and both throw - [getlogin_r](../../../core/libs/prx/libkernel/System/src/Login.cpp) (libkernel) returns the host effective account name: `GetUserNameW` as UTF-8 on Windows and `getpwuid_r(geteuid())` on POSIX. A valid account returns a name even without a login session; host `getlogin_r`, `USER` and `LOGNAME` are not consulted. The console's login name and behaviour without a session are unknown. Account lookup or encoding failures, a missing or empty account name, and POSIX account records exceeding the 16 KiB lookup buffer throw; there is no replacement name or retry. A short guest buffer returns `ERANGE` without modifying it, and a null output returns `EFAULT`. - [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 +- [hw.realmem](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctl` {`CTL_HW`, 12} and `sysctlbyname` report the same 8-byte value as `hw.physmem` (the `_SC_PHYS_PAGES` pages of 16 KiB), read-only with `EPERM` for a write and `ENOMEM` for a short buffer, as FreeBSD's [`sysctl_hw_realmem`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_mib.c#L191-L204) does. FreeBSD reports there the memory the firmware describes, which is at least `hw.physmem`. A payload process on a PS5 (firmware 12.70) gets -1 with `ENOENT` for `hw.physmem` and `hw.realmem` (a read, a size query and a null length) and for {`CTL_HW`, 5} and {`CTL_HW`, 12} (a read and a size query); what a title process gets was not measured. Dolphin reads it as the machine's memory size in [`MemPhysical`](https://github.com/dolphin-emu/dolphin/blob/8d2ad1a662472955497e5bb79d7b9ec6999e1f0d/Source/Core/Common/MemoryUtil.cpp#L253-L273) (RetroArch PPSA99169's `dolphin_libretro` core) +- [vm.overcommit](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` reports the 4-byte value 0, the default FreeBSD gives its [`vm.overcommit`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/swap_pager.c#L158-L161) `int`, with `ENOMEM` for a short buffer, and a write throws. A payload process on a PS5 (firmware 12.70) gets -1 with `ENOENT` for it, for a read, a size query and a null length; what a title process gets was not measured. mimalloc reads it in [`unix_detect_overcommit`](https://github.com/microsoft/mimalloc/blob/31d034d94cdb8e22f7d7ed55967f581a2d6e831d/src/prim/unix/prim.c#L129-L153) and takes 0 as no overcommit, as PS5MC (PPSA99420) does at start-up - [kern.sdk_version](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` reports the system software version as the 4-byte value `0x01000000` (1.00), the value `sceKernelGetSystemSwVersion` reports; a write returns -1 with `EPERM`. The major version is byte 3 and the minor version byte 2, in hex digits that read as decimal (13.60 is `0x1360xxxx`), as [Capy Mobile](https://github.com/khalifa007/CapyMobile/blob/2758c016/src/ps5/platform.c#L64-L71) (PPSA92280), [remote_lua_loader](https://github.com/n0llptr/remote_lua_loader/blob/7c2840e475/savedata/misc.lua#L195-L209) and the [lapse payload](https://github.com/NetflixNHack/Netflix-N-Hack/blob/605eb93ba5/PS4/payloads/lapse.js#L436-L450) read it. Capy Mobile only displays it. The console's firmware version is not modeled and the low two bytes are unknown - [kern.ostype, kern.osrelease, kern.version, kern.hostname, hw.machine](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctl` (1.1, 1.2, 1.4, 1.10, 6.1), `sysctlbyname` and `sysctlnametomib` serve the names `uname` reads as read-only strings: "FreeBSD", "11.4-RELEASE" and "FreeBSD 11.4-RELEASE\n", the values FreeBSD 11.4-RELEASE builds without source control information ([newvers.sh](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/conf/newvers.sh#L45-L52), [L293-L294](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/conf/newvers.sh#L293-L294)), the host name `gethostname` reports ("AnyPS5") and "amd64"; a short buffer returns `ENOMEM` and a write `EPERM`. The console's strings are not measured +- [kern.osreldate](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - reports MIB `{CTL_KERN, KERN_OSRELDATE}` as `1104001`, FreeBSD 11.4's `__FreeBSD_version` in `sys/sys/param.h`; this is consistent with `kern.osrelease` and `kern.version` above, but the value reported by a PS5 title has not been measured. - [sceKernelGetProsperoSystemSwVersion](../../../core/libs/prx/libkernel/System/src/SoftwareVersion.cpp) (libkernel) reports system software version `0x01000000` ("01.000.000"); the console's firmware version is not modeled. The 0x28-byte block (a 64-bit size at 0, the version string at 8 and the packed version at 0x24) follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Kernel/KernelSystem.cs), which Prospero Multi Tools (PPSA99110) and Prospero Explorer (PPSA99109) are built with and which sets the size to 40, and the same fields in [etaHEN](https://github.com/etaHEN/etaHEN/blob/main/Source%20Code/daemon/include/ipc.hpp) and [shsrv](https://github.com/ps5-payload-dev/shsrv/blob/master/sh.c). A size other than 0x28 returns `SCE_KERNEL_ERROR_EINVAL` without writing; whether the console checks the size is unverified, since etaHEN and shsrv declare a 0x30-byte block and do not set it - [sceKernelGetAllowedSdkVersionOnSystem](../../../core/libs/prx/libkernel/System/src/SoftwareVersion.cpp) (libkernel) reports the system software version with the 12 patch bits set, `0x01000fff`, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/98fd0689/src/core/libraries/kernel/kernel.cpp) does on the PS4 and as [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Kernel/KernelSystem.cs) describes it, which Prospero Explorer (PPSA99109) reads it through; a null pointer returns `SCE_KERNEL_ERROR_EINVAL`. The console's value and its errors are unverified - [sceKernelConvertUtcToLocaltime, sceKernelConvertLocaltimeToUtc](../../../core/libs/prx/libkernel/Time/Time.cpp) (libkernel) convert with the host time zone at the given instant, as libc `localtime` does, instead of the console's time zone setting. `west_sec` holds the standard-time offset in seconds east of UTC, despite its name, and `dst_sec` the daylight saving seconds in effect. This follows PPSA99203 ([PS5SX2](https://github.com/Swordpdf/PS5SX2/blob/b4cf55e87c34e8a2a1cd9302df4373e5a94d28f4/ps5/frontend/fe_ps5.cpp#L54-L63)), whose author saw a console write 7200 and 3600 into the 16-byte block; which zone that console was set to is not recorded, so whether `west_sec` includes daylight saving is not settled. [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/23c41333/src/core/libraries/kernel/time.cpp) also stores seconds east. PPSA99203 declares the last argument of `sceKernelConvertUtcToLocaltime` as `uint64_t*`; 32 bits are written. On Windows the standard offset and the daylight saving amount are the C runtime's current `_get_timezone` and `_get_dstbias` for every instant, as libc `localtime_s` uses them, so instants under a zone's earlier rules get today's offsets; a `TZ` variable does not set `_dstbias`, which keeps the value an earlier `_tzset` read from the system (0 when the system zone has no daylight saving), and summer time then adds nothing. In a repeated or skipped local hour, `sceKernelConvertLocaltimeToUtc` takes the offset in effect one standard offset earlier. Its last argument receives the zone's daylight saving amount whether or not it is in effect, found by sampling the host zone over the year around the result, while `dst_sec` is the amount in effect: the libc `mktime` that PPSA99109 bundles (text 0x70ebb-0x70ee5) subtracts that amount when `tm_isdst` is set outside summer time. That `mktime` passes `tm_isdst` as the second argument, which is ignored, so it does not choose the offset in a repeated hour. `sceKernelGettimezone` and the `gettimeofday` time zone still report no offset and no daylight saving. A host time the C runtime cannot convert throws @@ -48,38 +50,43 @@ - [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 -- [_umtx_op](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - `UMTX_OP_WAIT`, `UMTX_OP_WAKE`, `UMTX_OP_WAIT_UINT`, `UMTX_OP_WAIT_UINT_PRIVATE`, `UMTX_OP_WAKE_PRIVATE` and `UMTX_OP_NWAKE_PRIVATE` follow FreeBSD 11's [kern_umtx.c](https://github.com/freebsd/freebsd-src/blob/stable/11/sys/kern/kern_umtx.c): a timeout whose size argument is at most 16 is a relative `timespec`, a larger one an `_umtx_time`, and a wake of 0 or fewer wakes one waiter. `UMTX_OP_NWAKE_PRIVATE` reads the value as an `int` count of pointers at the object and wakes every waiter on each one; a count of 0 or fewer reads nothing, and a null list fails with `EFAULT`, where FreeBSD's `copyin` would also fail for any other unreadable list. Waiters share the queue of `sceKernelSyncOnAddressWait`. Operation 0 fails with `EOPNOTSUPP` and operations above 26 with `EINVAL`, as in FreeBSD 11; `UMTX_OP_WAIT_UINT` with a value that does not fit in 32 bits returns 0 at once, as FreeBSD's zero-extended compare does. Operation 1 throws, since PPSA12544's call_once uses it in a way FreeBSD rejects; the other operations throw, and so does a misaligned object. PPSA12544's `libc.prx` and Unity's `PS5Util.prx` (PPSA18194) pass `UMTX_OP_WAIT` and `UMTX_OP_WAIT_UINT` a pointer to a 4-byte microsecond timeout with size argument 0, which FreeBSD reads as a `timespec`, and wait for a running call_once with operation 1. Whether the console reads a microsecond timeout there in all cases, and what operation 1 does on it, is unverified. A call with size argument 0 is read unconditionally as a 4-byte microsecond timeout, matching callers like `libc.prx` and `PS5Util.prx`'s `zlqfTyrQSPk` wrapper which pass size 0 with a 32-bit microsecond count +- [_umtx_op](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - the mutex, condition variable, read-write lock, legacy lock and `SEM2` operations throw, and so does a misaligned object. Implemented as FreeBSD 11's [kern_umtx.c](https://github.com/freebsd/freebsd-src/blob/stable/11/sys/kern/kern_umtx.c): `UMTX_OP_WAIT`, `UMTX_OP_WAKE`, `UMTX_OP_WAIT_UINT`, `UMTX_OP_WAIT_UINT_PRIVATE`, `UMTX_OP_WAKE_PRIVATE`, `UMTX_OP_NWAKE_PRIVATE`, `UMTX_OP_SEM_WAIT` and `UMTX_OP_SEM_WAKE` (the last two built under `COMPAT_FREEBSD9`/`10`). Operations 0 and 1 fail with `EOPNOTSUPP`, as measured on a PS5 (firmware 12.70). The `SEM2` operations, 23 and 24, were measured on the console and returned `0` for a count of 0 where FreeBSD would time out, so they stay unimplemented. - [sceKernelSyncOnAddressWait](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - the only known caller passes a null timeout and a name string as the fourth argument; the timeout is assumed to point to microseconds like the other kernel waits, and the name is ignored - [sceKernelSyncOnAddressWait8 / sceKernelSyncOnAddressWait16 / sceKernelSyncOnAddressWait32 / sceKernelSyncOnAddressWait64](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - libc's `__std_atomic_wait_direct_8/16/32/64` pass a microsecond timeout or null and ignore the result; the by-value comparand, return values and the 2- and 8-byte alignment of Wait16 and Wait64 are assumed - [scePthreadRwlockDestroy, pthread_rwlock_destroy](../../../core/libs/prx/libkernel/Pthread/src/Rwlock.cpp) (libkernel) - measured on a PS5 (firmware 13.60): destroying a rwlock returns 0 whether it is free or held for reading or writing, and leaves 1 in the handle; every call on that handle then returns `EINVAL` until it is initialized again, and a zero handle is initialized on first use by the `scePthreadRwlock` calls as by the POSIX ones. A rwlock destroyed while held is not freed, so its memory leaks. Destroying a rwlock while another thread is acquiring it is not guarded - [scePthreadCondWait / scePthreadCondTimedwait](../../../core/libs/prx/libkernel/Pthread/src/Cond.cpp) (libkernel) - before waiting, FreeBSD libthr's [`cond_wait_common`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_cond.c#L1992-L2029) checks ownership through [`_mutex_owned`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_mutex.c#L1166-L1178): an unowned mutex returns `EPERM`, while its destroyed marker returns `EINVAL`. An untouched adaptive static initializer (slot value 1) therefore returns `EPERM`; the test covers this, an uninitialized slot, a mutex owned by another thread, and the destroyed marker for both timed and untimed sce and POSIX waits. A recursive mutex locked several times is released completely for the wait and relocked to the same depth afterwards, as FreeBSD libthr does (`_mutex_cv_unlock`, `_mutex_cv_lock`). These FreeBSD-derived behaviors remain unverified on console hardware. Cancellation follows the [POSIX condition wait contract](https://pubs.opengroup.org/onlinepubs/9799919799/functions/pthread_cond_clockwait.html): the original mutex and recursive depth are restored before cleanup, and a canceled waiter forwards a wake to the remaining waiters. The host condition state is retained across mutex reacquisition so the guest can destroy and free its condition after notification; cancellation ordering and signal replacement are not measured on a console - [sceKernelSetEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - multi-waiter satisfaction follows fpPS4 in-order evaluation: registered waiters are evaluated in FIFO order and each satisfied waiter's clear mode is applied immediately to the pattern before evaluating subsequent waiters; this ordering is unverified on PS5 console hardware +- [sceKernelOpenEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - returns the handle of an undeleted event flag this process created with the same name, and `SCE_KERNEL_ERROR_ENOENT` otherwise. Flags owned by other processes, such as the shell's `SceShellCoreUtilAppFocus`, do not exist here, and which attribute makes a flag openable on the console is not modelled. The error code for a missing name and the choice between flags sharing a name are unverified. - [sceKernelAioSubmitReadCommandsMultiple / sceKernelAioSubmitWriteCommandsMultiple / sceKernelAioWaitRequests / sceKernelAioPollRequests / sceKernelAioCancelRequest / sceKernelAioCancelRequests / sceKernelAioDeleteRequests](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) - behaviour from shadPS4 (one id per request for the Multiple submits, cancel turns any request aborted and id 0 reports processing, wait mode 2 returns once one request completed, poll copies every state without waiting); the 128 requests or ids per batch limit is from SharpProspero, with an unknown error, so larger batches throw; wait modes other than 1 (and) and 2 (or) throw; null pointers, negative counts and invalid ids return the codes the single-request siblings use, checked before any state is written -- [scePthreadAttr setters and getters](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) (libkernel) - measured on a PS5 (firmware 13.60): a null or uninitialized attribute returns `EINVAL`, as does a null output of the stack size, detach state and scheduling parameter getters; a policy other than 1 and 3, an inheritance other than 0 and 4, or a priority outside 256 to 767 (or a null parameter) returns `ENOTSUP` (`0x8002002D`); a detach state other than 0 and 1 and a stack size below 16384 return `EINVAL`; any affinity mask is accepted. A null attribute pointer to `scePthreadAttrInit` and a null output of `scePthreadAttrGetaffinity` crashed the payload, so they still throw. A new attribute holds a 64 KiB stack size on the console and 1 MiB here, which the host code that runs on guest threads needs +- [scePthreadAttr setters and getters](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) (libkernel) - measured on a PS5 (firmware 13.60): a null or uninitialized attribute returns `EINVAL`, as does a null output of the stack size, detach state and scheduling parameter getters; a policy other than 1 and 3, an inheritance other than 0 and 4, or a priority outside 256 to 767 (or a null parameter) returns `ENOTSUP` (`0x8002002D`); a detach state other than 0 and 1 and a stack size below 16384 return `EINVAL`; any affinity mask is accepted. A null attribute pointer to `scePthreadAttrInit` and a null output of `scePthreadAttrGetaffinity` crashed the payload, so they still throw. A new attribute holds a 64 KiB stack size on the console and 1 MiB here, which the host code that runs on guest threads needs. `scePthreadAttrGetschedpolicy` and `scePthreadAttrGetinheritsched` were not measured and follow [FreeBSD 11.4 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L216-L262); a null output of `scePthreadAttrGetinheritsched` throws, since libthr writes through it unchecked +- [scePthreadAttrGet](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) and [pthread_attr_get_np](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) - the returned scheduling policy is unmeasured on a console. Both report the recorded guest policy, following [FreeBSD 11.4 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L134-L164) +- [pthread_getprio](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) is unmeasured on the console. NID `aO+WS2xWuo4` is named in the firmware 3.20 [export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libkernel.c). Its return and errno contract follows FreeBSD 11 [thr_getprio.c](https://github.com/freebsd/freebsd-src/blob/671fb7df97631020c42ce55527dc49fffe0656b7/lib/libthr/thread/thr_getprio.c): return the recorded guest priority, or -1 with `ESRCH` for an unknown or finished thread. The live-thread registry lock protects the read from a concurrent join. This reports guest scheduler state, not the host scheduling priority. - [sceKernelGetAvailableCpumask](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - returns the default thread affinity, 0x1FFF (CPUs 0 to 12), as KytyPS5 does. Titles pin threads within it (PPSA02664 passes 0x1FFB, Hades II PPSA36082 0x3 and 0x3F); whether CPU 13 is available to a title is not confirmed - [sceKernelGetCurrentCpu](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - guest affinities are not applied to host threads, so the host CPU index is mapped into the calling thread's affinity clipped to `sceKernelGetAvailableCpumask` (the available mask when they do not overlap): host CPU n reports the (n modulo the CPU count)-th CPU of that mask, so a thread pinned to one CPU reports that CPU, as a FreeBSD `cpuset` guarantees on the console. Which CPU the console reports for a thread allowed on several is not known; shadPS4 returns 0 - [pthread_attr_setsolosched_np](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) takes `(attr, int)` and stores the value through `scePthreadAttrSetsolosched`, returning `EINVAL` (22) for a null or uninitialized attribute like the other POSIX attribute setters. Only the name is public (aerolib `2+pVfgiEd7A`); the signature and behaviour follow the SCE twin, which [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/947627d10d4ef4b68ba3c0189aab6c368e70a8d2/src/kernel/pthread.cpp#L2188-L2199) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/a2983692930564815a77f81e74c151268f5b935f/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs#L891-L913) store as an `int` without a range check, and [PS5PCEM](https://github.com/iStark/PS5PCEM/blob/71de7681f06c5db59a1120a3bb554c86780c3f5f/src/hle/libs/kernel_threading.zig#L1396-L1398) implements this alias over it. On a PS5 (firmware 13.60) only 0 and 16 are accepted; every other value returns `ENOTSUP` (45, `0x8002002D` from the SCE call) and keeps the stored one, and the SCE calls return `EINVAL` for a null or uninitialized attribute. PPSA15878 passes 16. Threads created from the attribute are not scheduled differently; what solo scheduling does on the console is unknown. Returnal (PPSA01285 v03.000.005) stopped on it with `what(): pthread_attr_setsolosched_np_nid_postfix not implemented` (#2872, #1155); LEGO Horizon Adventures (PPSA14632, #734) and Palworld (PPSA23126, #2598) import it -- [sceKernelMapFlexibleMemory / sceKernelMapNamedFlexibleMemoryInternal](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - flag 0x8000 is accepted and ignored; its meaning is not established. KytyPS5 (`GUEST_MAP_UNKNOWN_8000`) and sharpemu accept it without reading it. Every known caller is an SDK 9 `libc.prx` heap, which passes it with read/write protection and uses the mapping as an older `libc.prx` uses `sceKernelMapNamedFlexibleMemory` with flags 0: through the Internal variant in Astro Bot PPSA21564, through `sceKernelMapFlexibleMemory` in Animal Well PPSA22520 01.000.011 +- [sceKernelMapFlexibleMemory / sceKernelMapNamedFlexibleMemoryInternal](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - flag 0x8000 is accepted and ignored; its meaning is not established. KytyPS5 (`GUEST_MAP_UNKNOWN_8000`) and sharpemu accept it without reading it. Every known caller is an SDK 9 `libc.prx` heap, which passes it with read/write protection and uses the mapping as an older `libc.prx` uses `sceKernelMapNamedFlexibleMemory` with flags 0: through the Internal variant in PPSA21564, through `sceKernelMapFlexibleMemory` in PPSA22520 01.000.011 - [sceKernelMtypeprotect](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - sets the memory type of the direct mappings in the page-rounded range and of the direct memory behind them, as shadPS4 and prosper do (KytyPS5 ignores the type); the type is not validated, flexible pages in the range only get the protection, and a later map of the retyped direct memory reports the type it was allocated with. `sceKernelBatchMap2` operation 4 takes the same path with the entry type - [Map hints inside a reservation](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - `sceKernelMapDirectMemory` and the flexible and anonymous maps place a mapping without `MAP_FIXED` at its hint address when the whole range lies in a range still reserved by `sceKernelReserveVirtualRange`, as if `MAP_FIXED` were set. Taken from PPSA01474, which reserves 64 GiB and then maps its heaps inside it with hints and no `MAP_FIXED`, and uses the hint addresses afterwards; not measured on the console. Hints outside a reservation, or over a range that is partly mapped, stay hints - [Fixed anonymous mmap](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - `MAP_PRIVATE | MAP_ANON | MAP_FIXED` replaces the requested range through the existing guest mapping machinery and zeroes the new memory. `MAP_EXCL` is checked under the allocation-registry mutation lock, including virtual reservations, and returns `ENOMEM` without changing an occupied range; without `MAP_FIXED` it is `EINVAL`. Lengths are rounded to 16 KiB and fixed addresses must be aligned. The complete rounded range must lie inside the guest arena or, for POSIX fixed anonymous `mmap` on Windows, one current or registered native thread stack; other mapping APIs do not opt into stack replacement. Native stack replacement keeps the reservation, decommits and recommits fresh pages, and rejects foreign allocations, ranges spanning reservations and host `PAGE_GUARD` pages before mutation. Current-stack ranges must end at least 64 KiB below the active host frame; callers replacing another thread's stack must coordinate with that thread. If recommitting fails, the call returns `ENOMEM` with the range still reserved; thread cleanup attempts to recommit it before restoring write access. This follows FreeBSD 11.4 [argument validation and rounding](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_mmap.c#L226-L299), [fixed replacement](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_map.c#L1455-L1464), and [collision errno](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_mmap.c#L1604-L1613). The console behaviour has not been measured. AnyPS5 starts the guest arena at 0x200000000, so the fixed path rejects the low 0x400000 address requested by PICO-8 PPSA99808. The console address-space layout is unmeasured; executable placement outside the host arena is tracked in #5217. Static catalog disassembly finds `0x1012` in PPSA98001/PPSA99808 and `0x5012` in PPSA99808; these titles were not run. - [Mapping hints with no room above them](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - a mapping without `MAP_FIXED` whose hint lies above the guest address space, or has no free range at or above it, is placed from the bottom of the guest arena, as FreeBSD 11.4's [`vm_mmap_object`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_mmap.c#L1569) retries through [`vm_map_find_min`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_map.c#L1555-L1584) from its minimum address. FreeBSD's minimum is the end of the largest possible data segment rather than the start of the arena, and the console's address-space layout is not measured. PS5CEMU-HAR (PPSA99360) gives `sceKernelReserveVirtualRange` a hint near its own code, which is a host address here because the relinked executable lies outside the guest arena, so its JIT gets memory more than 2 GiB from its code and does not start - [_sceKernelRtldThreadAtexitIncrement, _sceKernelRtldThreadAtexitDecrement](../../../core/libs/prx/libkernel/System/src/Rtld.cpp) (libkernel) - the argument is the `dso` address a title's `libc.prx` passes to `__cxa_thread_atexit` (PPSA14632), which fails when Increment returns nonzero. As in the [fpPS4](https://github.com/red-prig/fpPS4) reimplementation, Increment takes a reference on the loaded image containing the address and Decrement releases one; the variants without the leading underscore are assumed to match - [tcgetattr, tcsetattr](../../../core/libs/prx/libkernel/File/src/Terminal.cpp) (libkernel) - every file and pipe descriptor is taken as not a terminal (`ENOTTY`), and a socket gets `EADDRNOTAVAIL` for `AF_INET` and `EOPNOTSUPP` otherwise, as FreeBSD's `in_control` and `pru_control_notsupp` answer a termios ioctl; what descriptors 0 to 2 of a title are on the console, and whether any of them is a terminal there, is unknown +- [cfsetspeed](../../../core/libs/prx/libkernel/File/src/Terminal.cpp) (libkernel, NID `C1MQ6OaXRjo`) - console behavior and module/library versions are unverified. The prototype, uint32 speed and 44-byte termios layout (input speed at 36, output speed at 40) follow the public PS5 payload SDK [termios.h](https://github.com/ps5-payload-dev/sdk/blob/a7685ce8e97ce1f8a4f38253a41d54241c9124f1/include/freebsd/termios.h) and [sys/_termios.h](https://github.com/ps5-payload-dev/sdk/blob/a7685ce8e97ce1f8a4f38253a41d54241c9124f1/include/freebsd/sys/_termios.h). The module association follows its libkernel stub table and the NID rehashes to the aerolib name. Both speed fields accept every uint32 value and no flags or control characters change, as [FreeBSD 11.4 termios.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libc/gen/termios.c) does. No terminal I/O is performed. Null pointers throw where the reference dereferences them. - [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) - descriptor 0 is never reported readable and descriptors 1 and 2 are always writable, as for a console title with no input; reads from descriptor 0 still go to the host stdin. `/dev/random` and `/dev/urandom` follow `randomdev_poll` for a seeded device under [`selscan`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_generic.c#L1178-L1236), which asks once with the flags of every set the descriptor is in: in the read set they are readable and reported in no other set, and otherwise they are reported in the write and exception sets they were asked in. A regular file, a directory or a file read from app0.zar is always readable and writable and never in the exception set, as `vop_stdpoll` answers, on every host. Guest sockets and libSceNet sockets are polled. A kqueue is readable while one of its events is triggered and is never writable or in the exception set, as FreeBSD 11.4's [`kqueue_poll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_event.c#L1900-L1924) answers, on every host. Other descriptors are polled through the host on Linux, while on Windows pipes are checked every millisecond with the readiness test `poll` uses and any other descriptor throws. Dungeons (PPSA16064) selects on descriptor 0, and RetroArch (PPSA99169) selects on its web server's wake-up pipe next to its listening socket - [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) - a descriptor the socket poller reports as unknown that is below `GuestSockets::FirstDescriptor` is polled on the host instead of throwing (on Windows only a pipe, as above); an invalid host descriptor fails with `EBADF`, and mixed host and socket sets are polled in short alternating slices so neither side waits out the full slice alone. A descriptor that stops being a socket, a kqueue or a pipe during the wait fails the call with `EBADF` when it is closed and is selected again with the rest of the timeout when its number was reused, as FreeBSD 11.4's [`kern_select`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_generic.c#L1129-L1140) looks every descriptor up again in each pass. A close that has already removed the descriptor from the guest's kqueue, pipe or file tables but not yet closed its host descriptor is seen by that new look-up as a descriptor of unknown kind, which throws on Windows and is polled through the host on Linux. Wobbly Life (PPSA29413) selects on file descriptors while streaming its bundles - [__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 - [sceCoredumpGetThreadContextInfo](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel, NID `ShChva57wIM`) takes `(void* threadContextInfo, size_t threadContextInfoSize)` as in the [Orbital type database](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json); the 144-byte block of 18 64-bit registers follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelSystem.cs#L120-L141). It answers as `sceCoredumpGetStopInfoCpu`: the handler is never invoked here, so an aligned 144-byte request returns `NOT_IN_COREDUMP_HANDLER` without reading or writing the block, and null, unaligned or wrapping buffers and other sizes throw, as the console validation order is unverified. Planet Coaster (PPSA01736) imports it - [sceCoredumpAttachMemoryRegion, sceCoredumpAttachUserMemoryFile](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) - both take `(uint32_t userValue, const void* mem, size_t size)`: for the first, the IDA type database of [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json) and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Kernel/KernelSystem.cs) bindings agree; the second has only SharpProspero (the PS4 5.05 firmware does not export it, so Orbital has no entry) and is described there as attaching the region as an unnamed user file. Not confirmed on a title; the RED engine's PS4 crash handler ([Vangaurd](https://github.com/vurimad/Vangaurd/blob/main/source/imported/common/redSystem/src/errorHandlerImplErrorHooksOrbis.cpp)) calls the sibling `sceCoredumpAttachMemoryRegionAsUserFile(999, buffer, bytes, name)` from inside its handler. Attaching belongs inside the core dump handler, which the system runs on its own thread after the process stops (SharpProspero's remark, the RED engine's usage and `SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER`, "function was not called from registered core dump handler", in the Vita's SceCoredump error table, [RivetDB](https://github.com/CreepNT/RivetDB/blob/master/rivetdb.json)). The handler is never invoked here, so every call is outside it and returns that error; nothing is recorded, as there is no dump to attach to. Its value `0x81180003` comes from shadPS4 (PS4), not confirmed on a PS5: [fpPS4](https://github.com/red-prig/fpPS4/blob/master/kernel/ps4_libkernel.pas) has `NOT_REGISTERED` 0x81180001 and `ALREADY_REGISTERED` 0x81180002, shadPS4's `coredump_error.h` has `NOT_IN_COREDUMP_HANDLER` = 0x81180003, and the Vita table numbers `PARAM` 0, `NOT_REGISTERED` 1, `ALREADY_REGISTERED` 2 and `NOT_IN_COREDUMP_HANDLER` 3. Whether the console checks the region before the calling context is unknown, so a null, empty or wrapping region throws. `userValue` is not interpreted; SharpProspero names its bits 31 and 30 as truncation flags for the user file variants -- [sceCoredumpWriteUserData, sceCoredumpDebugTextOut](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) - the signatures `ssize_t (const void* data, size_t size)` and `void (const char* str, int len)` follow the [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json) IDA type database and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelSystem.cs) bindings ("Repeated calls append", "The bytes written, or a negative error code"). User data is written from inside the core dump handler: the Vita's [`sceCoredumpWriteUserData`](https://github.com/vitasdk/vita-headers/blob/e66ebe90b73d1fa4cce005a5b2072cec27322544/include/psp2/coredump_nounlink.h) returns `0x800A0003` ("No user-data capture is active") outside it, and the RED engine's PS4 crash handler ([Vangaurd](https://github.com/vurimad/Vangaurd/blob/4f86d2b0e1a6c0d50526152c517a0292b1a68af4/source/imported/common/redSystem/src/errorHandlerImplErrorHooksOrbis.cpp)) makes all its coredump calls, `sceCoredumpDebugTextOut` included, from its handler. The handler is never invoked here, so `sceCoredumpWriteUserData` returns `SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER` (0x81180003 in shadPS4's [`coredump_error.h`](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/coredump/coredump_error.h), PS4 numbering) sign-extended to 64 bits and keeps nothing; whether the console checks the buffer before the calling context is unknown, so a null, empty or wrapping buffer throws. `sceCoredumpDebugTextOut` returns nothing; its text, which goes into the report on the console, is printed to stderr with the `[coredump]` prefix of `sceKernelDebugWriteCppExceptionInfo`, and a negative length or a null text with a nonzero length throws. No title is seen calling either: 007 First Light (PPSA11386) imports `sceCoredumpWriteUserData` and does not call it before its stop (#2523) +- [sceCoredumpWriteUserData, sceCoredumpDebugTextOut](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) - the signatures `ssize_t (const void* data, size_t size)` and `void (const char* str, int len)` follow the [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json) IDA type database and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelSystem.cs) bindings ("Repeated calls append", "The bytes written, or a negative error code"). User data is written from inside the core dump handler: the Vita's [`sceCoredumpWriteUserData`](https://github.com/vitasdk/vita-headers/blob/e66ebe90b73d1fa4cce005a5b2072cec27322544/include/psp2/coredump_nounlink.h) returns `0x800A0003` ("No user-data capture is active") outside it, and the RED engine's PS4 crash handler ([Vangaurd](https://github.com/vurimad/Vangaurd/blob/4f86d2b0e1a6c0d50526152c517a0292b1a68af4/source/imported/common/redSystem/src/errorHandlerImplErrorHooksOrbis.cpp)) makes all its coredump calls, `sceCoredumpDebugTextOut` included, from its handler. The handler is never invoked here, so `sceCoredumpWriteUserData` returns `SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER` (0x81180003 in shadPS4's [`coredump_error.h`](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/coredump/coredump_error.h), PS4 numbering) sign-extended to 64 bits and keeps nothing; whether the console checks the buffer before the calling context is unknown, so a null, empty or wrapping buffer throws. `sceCoredumpDebugTextOut` returns nothing; its text, which goes into the report on the console, is printed to stderr with the `[coredump]` prefix of `sceKernelDebugWriteCppExceptionInfo`, and a negative length or a null text with a nonzero length throws. No title is seen calling either: PPSA11386 imports `sceCoredumpWriteUserData` and does not call it before its stop (#2523) - [sceCoredumpAttachUserFile, sceCoredumpAttachMemoryRegionAsUserFile](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) - the signatures `int (uint32_t userValue, const char* path)` and `int (uint32_t userValue, const void* mem, size_t size, const char* name)` follow the [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json) IDA type database and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelSystem.cs) bindings. The crash handler calls both from inside its core dump handler, `sceCoredumpAttachUserFile(priority, absolutePath)` and `sceCoredumpAttachMemoryRegionAsUserFile(999, buffer, bytes, "LoggerQueueCrashDump.txt")`. The handler is never invoked here, so both return `SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER` (0x81180003, PS4 numbering from shadPS4) and attach nothing; whether the console checks the arguments before the calling context is unknown, so a null path or name and a null, empty or wrapping region throw. An empty path or name is passed through to that error, and `userValue` is not interpreted. No title is seen calling either - [sceKernelAddTimerEvent](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel) - `usec` is taken as the period in microseconds; re-adding an existing timer keeps its pending expiration count and restarts the period from the time of the call, where FreeBSD 12+ `filt_timertouch` clears pending expirations on `EV_ADD`; `usec == 0` fires on every wait. None of this is confirmed on a PS5 title - [sceKernelWaitEqueue](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel) - a level-triggered user event (`sceKernelAddUserEvent`) is reported at most once per call, as in FreeBSD `kqueue_scan` (`sys/kern/kern_event.c`), which re-queues a reported knote without `EV_CLEAR` or `EV_DISPATCH` behind the scan marker, and kevent(2), where several triggers do not produce several kevents. Not measured on a console. Events are scanned in list order and the list is not rotated, so when `num` is smaller than the number of ready level events the later ones can starve, where FreeBSD moves a reported level knote to the tail. Several completions of one AMPR or VideoOut event within one call are returned as separate entries, where FreeBSD would aggregate them; the Orbis behaviour is unconfirmed @@ -102,34 +109,38 @@ - [uDGXwUKqi8s](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - unknown name, signature - [sceKernelGetCpuTemperature, sceKernelGetSocSensorTemperature](../../../core/libs/prx/libkernel/System/src/ThermalSensors.cpp) (libkernel) - read the host's thermal sensors: on Linux from `/sys/class/hwmon` and `/sys/class/thermal`, on Windows from the ACPI thermal zones (`MSAcpi_ThermalZoneTemperature` over WMI) and the storage temperature property of the first physical drive. Sensor 0 is the CPU/package value, 1 the first other value and 2 an NVMe value; a host that exposes none reports every sensor as unsupported (`EINVAL`). The console's SoC channel count and meanings are hardware-dependent and differ from the host's, the host sensors are not the console's, the ACPI reading is unavailable on many machines, and the storage temperature property is numbered 52 on Windows 10 2004 and later and 22 before it, so the mapping, the values and the unsupported-input error are unverified; the storage channel was checked against the drive's own SMART composite temperature on one NVMe host (`StorageDeviceTemperatureProperty`, 36 °C on a WD_BLACK SN7100). `sceKernelGetCpuTemperature` reports sensor 0. PPSA99058 and PPSA99420 call both - [LinuxProtFromSce](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - protection bits 0x100 and 0x200 are accepted and assumed not to affect host CPU access; host protection is derived only from the read, write and execute bits -- [inet_pton, inet_ntop](../../../core/libs/prx/libkernel/Socket/src/AddressConversion.cpp) (libkernel) - the text forms are assumed to be those of FreeBSD 11.4 and are unverified on a console: `inet_pton` accepts only four decimal parts without leading zeros for `AF_INET` and the RFC 4291 forms for `AF_INET6` (one `::` standing for at least one group, a dotted-quad tail only as the last 32 bits, no zone id), and `inet_ntop` prints lowercase hex with the first longest run of two or more zero groups compressed, and a dotted tail only for `::a.b.c.d` and `::ffff:a.b.c.d`. They no longer depend on the host resolver library - [sceKernelIsThreadSanitizerEnabled](../../../core/libs/prx/libkernel/System/src/Sanitizer.cpp) (libkernel, NID `LzoM-wVLJDE`) - no source gives the signature; `int (void)` is assumed from `sceKernelIsAddressSanitizerEnabled` and from PPSA23226's bundled `libSceJobManager_nosubmission.prx`, which passes no arguments and tests only the 32-bit result - [getdtablesize](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - returns the smaller of the `RLIMIT_NOFILE` soft limit of `getrlimit` and the host's `getdtablesize()`, capped at `INT_MAX`: FreeBSD returns the smaller of the `RLIMIT_NOFILE` soft limit and `maxfilesperproc` ([`sys_getdtablesize`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L338-L355)), and the host call supplies that bound on macOS, where the soft limit alone is not one (`setrlimit` accepts soft limits above `maxfilesperproc`, `RLIM_INFINITY` included); `maxfilesperproc` is not measured on the console. The guest limit is the host's soft limit, raised to the hard limit when libkernel loads only on Linux: `core/libs/prx/libkernel/File/src/OpenFileLimit.cpp` is `__linux__`-only, so on macOS the guest gets whatever soft limit the host process inherited. VLC (PPSA85300) imports it for a `closefrom` loop in the child of its spawn helper ([libc_stubs.c](https://github.com/ZiZc3/VLC/blob/d46be6a68f4244466b041c2dc1b640915ac1a983/ps5/compat/libc_stubs.c#L66-L72)), which never runs because its `fork` fails with `ENOSYS` - [close, sceKernelClose](../../../core/libs/prx/libkernel/File/src/HostStreams.cpp) (libkernel) - descriptors 0 to 2 that are open when libkernel loads belong to the host process: closing one fails with `EBADF` and leaves it open. The console's result is unknown. PPSA21703 calls `sceKernelClose(0)` repeatedly; closing the host stream let the host C runtime give descriptor 0 to the next file opened, which the next call then closed +- [sceKernelPrintBacktraceWithModuleInfo](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - imported by PS4 titles, which do not load without it, and added without an implementation: declared without parameters, every call throws ### Functional +- [scePthreadYield](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) on Windows calls Sleep(1) after 512 consecutive yields that find no other thread ready on the current processor and arrive less than ~10,000 TSC ticks apart (so the span varies with host CPU TSC rate); FreeBSD sched_yield never sleeps. - [Guest thread scheduling](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - the policies and priorities that `sched_get_priority_max`, `sched_get_priority_min`, `pthread_setschedparam` and `scePthreadSetschedparam` accept, and those a created thread takes, follow a PS5 (firmware 13.60), but they are only recorded and reported: they are not applied to host threads, so a title's priority order has no effect on host scheduling - [sceKernelMprotect, sceKernelMtypeprotect](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) over a range with a gap that is not registered in the allocation tracker protect the registered parts and skip the gap, and return 0, as FreeBSD 11.4's [`vm_map_protect`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/vm/vm_map.c) does (it has no check for holes); a range whose start is unmapped (also when later pages are mapped) returns `SCE_KERNEL_ERROR_EINVAL` (`mprotect`: `EINVAL`) because an existing test pins that for an unmapped start; FreeBSD answers 0 there too (`vm_map_protect` takes the next entry), so this errno is not taken from the source and was not changed. `sceKernelMprotect` threw for a gap before, which ended the title. Every part is checked for a GPU or image pin before the first one is protected; the parts are then protected one after the other, so a host protection failure in a later part leaves the earlier parts changed (FreeBSD's `vm_map_protect` cannot fail in its second pass). The behaviour on the console is not known. None of this was measured on a console. - [sceKernelMprotect protection bits](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - bits outside `0x3F7` are dropped instead of refused, as a PS5 (firmware 13.60) drops them: Onimusha: Way of the Sword (PPSA27836 01.000.001) maps direct memory with `0xf2` and then protects it with `0xfffffff2`, which the console reports as `0xf3` and then `0x3f3`. In a payload process the console also drops the CPU execute bit (`0x4`) of flexible, direct and JIT memory, and POSIX `mprotect` with write and execute reports write only (`0x2`). A title process may keep execute, so it is kept here. The map calls and `sceKernelMtypeprotect` refuse undefined bits on the console with `EINVAL` (`0x80020016`), and the direct map calls refuse execute; here both throw for undefined bits, and the direct map calls map execute - [JIT shared memory](../../../core/libs/prx/libkernel/DirectMemory/Jit.cpp) (libkernel) - `sceKernelJitCreateSharedMemory`, `sceKernelJitCreateAliasOfSharedMemory` and `sceKernelJitMapSharedMemory` take the signatures of [Orbital](https://github.com/AlexAltea/orbital)'s `tools/ida/db_types.json` (name, length, maximum protection, descriptor out; descriptor, maximum protection, descriptor out; descriptor, protection, address in and out), which ps4sdk, ps5-jar-loader and the call sites in RetroArch PPSA99169, XPSemu PPSA97358 and VLC PPSA85300 match; SharpProspero declares the alias as returning the descriptor. The object is a host anonymous file shared by its descriptors (a `memfd` on Linux, a delete-on-close temporary file mapped as an executable section on Windows), the alias is a duplicate descriptor, and every map maps the whole object, so writes through one view show through the others; a non-null address is a hint, as in `mmap` without `MAP_FIXED`. Not measured on the console: a length that is zero or not a multiple of 16 KiB and protection bits other than CPU read, write and execute are `EINVAL`; an alias or a map asking for more than the descriptor's maximum protection (write and execute counting as read) is `EACCES`, as FreeBSD 11's `vm_map_protect` treats a protection past `max_protection`; a descriptor that is not a JIT object is `EBADF`. The name is ignored, the objects do not count against the flexible memory budget, `mprotect` of a view is not limited to the maximum protection, and `mmap` of the descriptors maps the object as a [regular file](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp), so its views and the JIT views see each other's writes. On Linux the `memfd` is created without `MFD_EXEC`, which Linux 6.3 added and older kernels reject with `EINVAL`, and relies on the protection of the mapping: the flag only sets the file's execute permission bits, and the direct memory `memfd` is mapped executable the same way; a host with `vm.memfd_noexec` set to 2 refuses the creation and the call throws -- [kqueue and kevent](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) follow FreeBSD 11's [`kern_event.c`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/kern_event.c) (`kqueue_kevent`, `kqueue_register`, `kqueue_scan`, the timer and user filters) over the event queue; nothing is measured on a console. The descriptor is a host null device registered as the kqueue: `close` and `sceKernelClose` close the queue and `dup`/`dup2` of it throw, but `read`, `write` and `fstat` act on the null device instead of failing with `EOPNOTSUPP` or reporting `S_IFIFO`. Read and write events are supported on guest sockets only, plus `EVFILT_READ` on the read end of a pipe (see the kevent on a pipe entry) and the timer, user and [vnode](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) filters and a read event on the host stream on descriptor 0, which never triggers, as `select` never reports descriptor 0 readable; PPSA34015 registers descriptor 0 for reading in a new kqueue at start-up and does not check the result; other filters, `EV_CLEAR` on a socket or a pipe, `EVFILT_READ` on another kqueue, a `NOTE_NSECONDS` period above one second with a fraction (11.0 converts the fraction with the microsecond macro) and any change to an existing timer (11.0 keeps the armed expiry and applies the new period after it, 11.4's `filt_timertouch` restarts it) throw. `EV_DISABLE`, `EV_DISPATCH` and `EV_FORCEONESHOT` follow kern_event.c: a disabled knote stays registered, stops polling or firing and is not reported, a trigger raised while it is disabled is kept and reported after `EV_ENABLE`, where `knote` would drop it, `EV_DISPATCH` reports once and then disables the knote, and `EV_FORCEONESHOT` is ignored on a new knote while on an existing one it sets `EV_ONESHOT` and fires the knote; a negative `nevents` fails with `EINVAL`. A new user event keeps its `fflags` unmasked and is not triggered by `NOTE_TRIGGER`, because `filt_userattach` reads the already cleared `kn_fflags`. The timer limit `kq_calloutmax` (`ENOMEM`) is not enforced, and closing a kqueue wakes a thread waiting on it in `kevent` with `EBADF`, where `kqueue_drain` makes the close wait for that thread. A level-triggered event is reported once per call and moved behind the other events, as in `kqueue_scan`; the `sceKernel*Equeue` calls keep their own collection loop. `NOTE_LOWAT` on a socket sets the low-water mark as FreeBSD 11's [`filt_soread` and `filt_sowrite`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/uipc_socket.c) do, and a later change of the knote replaces it as `kqueue_register` replaces `kn_sdata`: a read event fires once the buffered bytes reach the mark, or at end of file or on a listening socket. A read mark above 1 is taken on stream sockets only: on a datagram socket Linux's `FIONREAD` reports the next datagram where FreeBSD's `sbavail` counts every queued byte, so it throws. A write event takes a mark of 1 only, where the host's writability stands in for one free byte, since the host does not report the send buffer's free space; other write marks and marks below 1, which make FreeBSD's read filter always fire, throw +- [kqueue and kevent](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) follow FreeBSD 11's [`kern_event.c`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/kern_event.c) (`kqueue_kevent`, `kqueue_register`, `kqueue_scan`, the timer and user filters) over the event queue; nothing is measured on a console. The descriptor is a host null device registered as the kqueue: `close` and `sceKernelClose` close the queue and `dup`/`dup2` of it throw, but `read`, `write` and `fstat` act on the null device instead of failing with `EOPNOTSUPP` or reporting `S_IFIFO`. Read and write events are supported on guest sockets only, plus `EVFILT_READ` on the read end and `EVFILT_WRITE` on the write end of a pipe (see the kevent on a pipe entry) and the timer, user and [vnode](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) filters and a read event on the host stream on descriptor 0, which never triggers, as `select` never reports descriptor 0 readable; PPSA34015 registers descriptor 0 for reading in a new kqueue at start-up and does not check the result; other filters, `EV_CLEAR` on a socket or a pipe off Linux (see the kevent EV_CLEAR entry), `EVFILT_READ` on another kqueue, a `NOTE_NSECONDS` period above one second with a fraction (11.0 converts the fraction with the microsecond macro) and any change to an existing timer (11.0 keeps the armed expiry and applies the new period after it, 11.4's `filt_timertouch` restarts it) throw. `EV_DISABLE`, `EV_DISPATCH` and `EV_FORCEONESHOT` follow kern_event.c: a disabled knote stays registered, stops polling or firing and is not reported, a trigger raised while it is disabled is kept and reported after `EV_ENABLE`, where `knote` would drop it, `EV_DISPATCH` reports once and then disables the knote, and `EV_FORCEONESHOT` is ignored on a new knote while on an existing one it sets `EV_ONESHOT` and fires the knote; a negative `nevents` fails with `EINVAL`. A new user event keeps its `fflags` unmasked and is not triggered by `NOTE_TRIGGER`, because `filt_userattach` reads the already cleared `kn_fflags`. The timer limit `kq_calloutmax` (`ENOMEM`) is not enforced, and closing a kqueue wakes a thread waiting on it in `kevent` with `EBADF`, where `kqueue_drain` makes the close wait for that thread. A level-triggered event is reported once per call and moved behind the other events, as in `kqueue_scan`; the `sceKernel*Equeue` calls keep their own collection loop. `NOTE_LOWAT` on a socket sets the low-water mark as FreeBSD 11's [`filt_soread` and `filt_sowrite`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/uipc_socket.c) do, and a later change of the knote replaces it as `kqueue_register` replaces `kn_sdata`: a read event fires once the buffered bytes reach the mark, or at end of file or on a listening socket. A read mark above 1 is taken on stream sockets only: on a datagram socket Linux's `FIONREAD` reports the next datagram where FreeBSD's `sbavail` counts every queued byte, so it throws. A write event fires once the send buffer's free space reaches the mark, as `filt_sowrite` does with `sbspace`; the host reports that space as `SO_SNDBUF` less `SIOCOUTQ` on Linux and as `SO_SNDBUF` alone elsewhere, so off Linux it does not shrink while data is queued. Marks below 1, which make FreeBSD's read filter always fire, throw - [EVFILT_VNODE, sceKernelAddFileEvent and sceKernelDeleteFileEvent](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) (libkernel) watch the host file or directory behind the descriptor and turn host changes into FreeBSD 11 vnode hints, which [`filt_vfsvnode`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/vfs_subr.c#L5032) adds whole when they share a bit with the requested `fflags`: a write that grows a file is `NOTE_WRITE | NOTE_EXTEND`, another write `NOTE_WRITE`, a shrinking truncate or an attribute change `NOTE_ATTRIB`, a higher link count `NOTE_LINK`, a lower one or a pending delete `NOTE_DELETE`, a new path `NOTE_RENAME`; in a directory, an entry created, removed or renamed is `NOTE_WRITE`, with `NOTE_LINK` for a subdirectory, and on Linux an entry moved from or to another directory adds `NOTE_EXTEND`; inotify queues the two halves of a rename one after the other, so a watcher woken by the first can read it alone, and a half read without the other is classified only after a `getdents64` on the watched directory, which takes the directory lock that `rename` holds until both halves are queued ([`do_renameat2`](https://github.com/torvalds/linux/blob/v6.8/fs/namei.c#L4975) around `vfs_rename` and its `fsnotify_move`, [`iterate_dir`](https://github.com/torvalds/linux/blob/v6.8/fs/readdir.c#L103)), an ordering taken from the kernel source, not from inotify(7), which promises none; a directory registered with `NOTE_EXTEND` on Windows is accepted, as `vfs_kqfilter` does not check it, but never receives it, because the directory handle cannot tell a move between directories from a removal plus an addition. FreeBSD's hints depend on the filesystem and the console's filesystems are not known: a growing `ftruncate` reports `NOTE_WRITE | NOTE_EXTEND` where `vop_setattr_post` reports `NOTE_ATTRIB`, and on Windows `utimes` on a file reports `NOTE_WRITE`. The three Windows notifications are told apart in the error, which names the `parent`, `entries` or `subdirectories` watch, the path it was given and `GetLastError`, but a `FindFirstChangeNotification` that fails still throws rather than returning an error to the guest: the EVFILT_VNODE attach path is [`vfs_kqfilter`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/vfs_subr.c#L4936), which returns `EINVAL`, `ENOMEM` or 0 after `fget` has turned a descriptor that is not open into `EBADF`, and consults no directory permission, so no FreeBSD errno stands for a host that refuses to notify, and what the console does on such a host is not measured. Only the `parent` watch can fail this way in practice: `entries` and `subdirectories` need the `FILE_LIST_DIRECTORY` that opening the watched directory already took. The parent handle produces no hint of its own, as `RunHostWatch` only calls `FindNextChangeNotification` on it, so a later change may leave the watch on its 50 ms poll instead of throwing; that is not done here. Windows uses `FindFirstChangeNotification` on the parent directory (and on a watched directory itself) and compares `GetFileInformationByHandleEx` snapshots at least every 50 ms, because a write through an open handle is reported only when the handle is flushed or closed, so file hints can arrive 50 ms late, and a file deleted while watched, which the POSIX delete of Windows 10 1903 moves into `$Extend\$Deleted` until its last handle closes, reports `NOTE_DELETE` without `NOTE_RENAME` and keeps its handle without a parent directory watch, as that directory cannot be watched; Linux uses inotify on the file and is not run. `NOTE_REVOKE`, `NOTE_OPEN`, `NOTE_CLOSE`, `NOTE_CLOSE_WRITE`, `NOTE_READ`, unknown bits and descriptors other than files, directories, pipes and sockets throw. `sceKernelAddFileEvent(eq, fd, watch, udata)` and `sceKernelDeleteFileEvent(eq, fd)` take the [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9d/include/orbis/libkernel.h#L35) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0/src/SharpProspero/Interop/Kernel/KernelEqueue.cs#L164) signatures; `watch` is taken as the `fflags` and the event is added with `EV_CLEAR`, neither confirmed on a title (feat(libkernel): implement kevent EV_DISABLE, EV_DISPATCH and EV_FORCEONESHOT) (fix(libkernel): follow kqueue_register for EV_FORCEONESHOT and modify flags) - [sem_timedwait](../../../core/libs/prx/libkernel/Pthread/Posix/Sem.cpp) (libkernel) consumes an available count before checking the timeout and waits indefinitely for a null timeout, following FreeBSD 11's `sem_timedwait`. Console behavior for null and invalid timeouts when a count is available is unverified. - [tcsendbreak, tcdrain, tcflush, tcflow, tcgetpgrp, tcsetpgrp](../../../core/libs/prx/libkernel/File/src/Terminal.cpp) (libkernel) follow FreeBSD 11.4's [`termios.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/termios.c): `tcflush` answers `EINVAL` for a queue other than `TCIFLUSH`, `TCOFLUSH` or `TCIOFLUSH` and `tcflow` for an action outside `TCOOFF`..`TCION` before the descriptor is looked at, and every other call fails as its terminal ioctl does on a descriptor that is not a terminal, with the errors `tcgetattr` gives, so `tcsendbreak` never reaches its 0.4 s pause. FreeBSD's `pipe_ioctl` answers `TIOCGPGRP` and `TIOCSPGRP` on a pipe or FIFO with the descriptor's SIGIO owner, which is not modelled, so `tcgetpgrp` and `tcsetpgrp` throw on a host pipe or FIFO above descriptor 2; descriptors 0 to 2 are taken as non-terminals that are not pipes, whatever the host attaches to them, so a pipe the guest creates on a freed descriptor 0 to 2 gets `ENOTTY` there. The PS5 `libkernel` is assumed to use this FreeBSD code, and `tcdrain` is not a cancellation point here - [Windows file timestamps](../../../core/libs/prx/libkernel/File/include/WindowsFileTime.hpp) use the host filesystem's timestamp range and resolution. `utimes`, `futimes`, and `lutimes` reject timestamps outside nonnegative signed 64-bit Windows file-time ticks with `EINVAL`, including exactly 1601-01-01 00:00:00 UTC, whose zero value means "leave unchanged" to `SetFileTime`. Directory timestamp updates resolve the current path of the opened handle. Filesystems with coarser timestamp resolution cannot retain every guest microsecond - [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. +- [readv, writev, preadv, pwritev and their sceKernel forms](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) - unreadable iovec-table errors follow FreeBSD 11.4's [`copyinuio`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/subr_uio.c#L401-L434), not console measurements. The table is copied before inspecting its entries; payload-buffer validation is unchanged. A Linux `process_vm_readv` failure other than `EFAULT` throws, including when the host denies that API. - [/dev/random and /dev/urandom](../../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) (libkernel) opened for reading return a descriptor backed by the host null device whose reads are filled with host OS entropy (`getentropy`, `rand_s` on Windows); both paths behave the same, never block, and positioned reads ignore the offset (FreeBSD `random(4)`). Only the exact paths are recognised, `fstat`, `lseek` and `mmap` see the null device, and opening them for writing throws - [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 +150,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 50 ms after the raise began, such as a long HLE or driver call, is then redirected in host code anyway, with the same hazard. A target seen right after a `syscall` instruction in ntdll, inside a system call that is not a wait such as the `NtWriteFile` of an `fwrite` holding its stream lock, restarts those 50 ms, so a raise can cost the raising thread up to the 1 s retry limit. While the target is suspended only `VirtualQuery` and a lock-free cache of image bases are consulted; an image not yet in the cache is classified with `GetModuleFileNameW` after the target is resumed. An APC queued at the first host-code sample, which an alertable guest wait would run sooner, is not implemented - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) builds the handler's `ucontext` with FreeBSD's amd64 layout (16-byte sigmask, `mcontext` at 0x40, `mc_rsp` at 0xf8); the console's layout is not verified beyond the fields PS5Util reads - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) does not consult the target thread's signal mask - [pthread_sigmask](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) shares the process-wide mask of `sigprocmask` and `_sigprocmask`, which keeps only the first 32 signals; FreeBSD keeps a mask per thread @@ -150,34 +163,38 @@ - [sigaltstack](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records each thread's alternate signal stack and reports it back, following FreeBSD's `kern_sigaltstack` (`EINVAL` for flags other than `SS_DISABLE`, `ENOMEM` below `MINSIGSTKSZ` 2048), but handlers never run on it, so `SS_ONSTACK` and `EPERM` are never reported - [sigpending](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) reports the intersection of the process-wide pending set of `sigtimedwait` and the blocked mask of `_sigprocmask`, as FreeBSD's `sys_sigpending` intersects the pending list with `td_sigmask`, and returns -1 with `EFAULT` for a null set. Because `_sigprocmask` keeps only the first 32 signals, signals 33 to 128 are never blocked and never appear in the set, and setting `SIG_IGN` (or `SIG_DFL` on a signal whose default action is to ignore) does not clear a signal that was already marked pending. PPSA99420 imports it - [scePthreadJoin, scePthreadCreate](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - on a PS5 (firmware 13.60) joining a thread a second time returns `ESRCH` (`0x80020003`), as it does here, but a joined thread is kept until `scePthreadCreate` publishes over the same handle storage, so one host object and its handle are retained per distinct guest handle storage location for the lifetime of the process; a guest that joins into a fresh storage location every time keeps every one of them. A null entry crashed the payload and still throws. Joining the calling thread returns `EDEADLK` and a zeroed attribute creates a default thread, as on the console. `scePthreadDetach` on a joined thread answers `EINVAL`, as FreeBSD does on a thread that is not joinable; the console answer for a detach after a join is unverified. Detaching a thread while another thread waits to join it returns `EINVAL`, and a second concurrent join returns `EOPNOTSUPP`, as FreeBSD 11.4's [`thr_detach.c`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_detach.c) and [`thr_join.c`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_join.c) check `joiner`; before this PPSA34015 stopped once with `Joining guest thread: Access is denied` when a thread it was joining was released under the join, and which of the two calls released it was not captured. A detached thread is still released when it ends, so joining it after it ended reads freed memory. A handle value copied to a second storage and joined there retains the object under the original storage key, so a later `scePthreadCreate` over that original storage releases an object the copy still points at, and the copy reads freed memory -- [Thread cancellation](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) follows FreeBSD libthr (states and types 0/1 and 0/2, `PTHREAD_CANCELED` 1, the `_pthread_cleanup_info` node layout), not measured on the console. A request is acted on only in `scePthreadTestcancel`, the condition variable, `sem` and `scePthreadSem` waits, `scePthreadJoin` and the sleeps, and their POSIX names; file and socket calls are not cancellation points. A sleep is not interrupted: the thread exits when it ends. The asynchronous type acts like the deferred one, except at the two set calls. +- [Thread cancellation](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) follows FreeBSD libthr (states and types 0/1 and 0/2, `PTHREAD_CANCELED` 1, the `_pthread_cleanup_info` node layout), not measured on the console. A request is acted on only in `scePthreadTestcancel`, the condition variable, `sem` and `scePthreadSem` waits, `scePthreadJoin` and the sleeps, and their POSIX names; file and socket calls are not cancellation points. An enabled cancellation request wakes POSIX `sleep` and exits the thread. `usleep`, `sceKernelUsleep`, `nanosleep`, `_nanosleep`, `sceKernelNanosleep` and `sceKernelSleep` check cancellation only before and after the host sleep, so an enabled request during those waits is delivered when the sleep ends. The asynchronous type acts like the deferred one, except at the two set calls. - [cpuset_setaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1281-L1286) thread selection. Bits outside `sceKernelGetAvailableCpumask` are dropped, and a mask with none of those bits fails with `EDEADLK`, as FreeBSD does when the new set has no CPU in common with the parent set. Guest-TID lookup and the update hold the thread registry lock so a join cannot release the target during access. Affinity is recorded for guest CPU reporting and child inheritance, not applied to host scheduling; PS5 behaviour has not been measured - [cpuset_getaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L426-L437) guest-TID selection, not measured on PS5. For the process the mask is the union of the affinities of the live guest threads, as [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1150-L1156) ORs the mask of every thread in the process; threads the guest did not create through libkernel are not included, and the console result is unverified - [pthread_setaffinity_np](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - follows [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_affinity.c#L41-L62) libthr over `kern_cpuset_setaffinity` for the calling thread and any live guest thread: a size other than the 32-byte `cpuset_t` returns `ERANGE`, an unknown or finished thread `ESRCH`. Bits outside `sceKernelGetAvailableCpumask` are dropped and a mask with none of those bits returns `EDEADLK`, as `cpuset_setaffinity` does; the affinity is recorded on the guest thread and not applied to the host thread, and the console result is unverified -- [dup and dup2](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) throw for the combinations they do not model: `dup2` of a guest socket onto descriptors 0 to 2, whose host streams libc writes without the socket table, and on Windows duplicating a directory descriptor. Guest sockets share one socket object between the duplicates; the numbers come from the host descriptor table like those of files. The errno values for descriptor exhaustion are not verified against the console. +- [dup and dup2](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) throw for the combinations they do not model: `dup2` of a guest socket onto descriptors 0 to 2, whose host streams libc writes without the socket table, and on Windows duplicating a directory descriptor. Guest sockets share one socket object between the duplicates; the numbers come from the host descriptor table like those of files. The errno values for descriptor exhaustion are not verified against the console. On Windows a guest `dup2` onto descriptor 1 or 2 keeps host logging on an `_fdopen` of a duplicate, and the guest `FILE` stays on the descriptor that was replaced; opening that duplicate throws (#7791) - [waitpid](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) returns -1 with `ECHILD` (`EINVAL` for unknown options): nothing here creates a child process; child processes would need a real `waitpid`. - [execv](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws: replacing the process image is not supported, the same as `execvp`. PPSA97358 imports it and fails to load without it. - [execve](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws like `execvp`: replacing the process image is not supported. The console behavior for invalid arguments and executable path lookups is not modeled. - [sysarch](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - `AMD64_GET_FSBASE`, `AMD64_GET_GSBASE` and `AMD64_SET_GSBASE` follow FreeBSD 11's amd64 [`sysarch`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/amd64/amd64/sys_machdep.c) on Linux x86-64 hosts, through the host's `arch_prctl`. The GS base belongs to the calling thread, a base at or above `0x800000000000` (`VM_MAXUSER_ADDRESS`) returns `EINVAL`, and a null argument returns `EFAULT`. `AMD64_SET_FSBASE` throws, since guest and host code share the thread's FS base for their TLS. In a payload process on a PS5 (firmware 12.70), `I386_GET_FSBASE` (7) and `I386_GET_GSBASE` (9) wrote 4 bytes, the low 32 bits of the FS and GS bases, and failed with `EFAULT` for a null argument; here they do the same on Linux x86-64 hosts. There a GS base set by one thread left the main thread's unchanged, and threads made with `pthread_create` and `scePthreadCreate` started with their creator's GS base; here a guest thread's host thread is created by the creating thread, and Linux's [`copy_thread`](https://github.com/torvalds/linux/blob/v6.6/arch/x86/kernel/process.c#L182-L186) gives a new thread its creator's GS base. The operation numbers FreeBSD 11's amd64 `sysarch` has no case for returned `EINVAL` there for the nine tried, each with a null argument (2, 5, 6, 12, 127, 133, 134, 1000 and -1; 6 is `I386_VM86`, which its header names), as its [default case](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/amd64/amd64/sys_machdep.c#L324-L325) does; here every such number returns `EINVAL` on every host; the numbers other than those nine follow that default case and were not measured. `AMD64_GET_XFPUSTATE` (132) returned 0 there for lengths 0, 64, 128, 256 and 512 (512 was also the largest length a search accepted) and failed with `EINVAL` for 1024, 2048, 4096 and 8192 and with `EFAULT` for a null argument or address, and `I386_GET_XFPUSTATE` (11) failed with `EFAULT` for a null argument; both throw here. The other operations FreeBSD 11's amd64 `sysarch` handles, and every operation it handles on other hosts (Windows keeps its TEB in GS), throw. - [kern.proc.pathname](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) returns `/app0/eboot.bin` for pid -1 and the calling process, following FreeBSD's `sysctl_kern_proc_pathname`; the exact pathname on retail consoles remains unverified. -- [machdep.cacheline_size](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) returns -1 with `ENOENT`, matching FreeBSD amd64 where this sysctl OID is not defined. +- [kern.proc.vmmap](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctl` {`CTL_KERN`, `KERN_PROC`, `KERN_PROC_VMMAP`, pid} returns packed FreeBSD 11 `kinfo_vmentry` records (144 bytes each, an empty path) for the used ranges of the guest address-space arena, in address order, as [`kinfo_getvmmap`](https://github.com/freebsd/freebsd-src/blob/releng/11.0/lib/libutil/kinfo_getvmmap.c) reads them. A range is one record: the type is always `KVME_TYPE_DEFAULT`, the protection is `sceKernelVirtualQuery`'s for the range's first page (0 where it reports none), and offset, object, resident page counts, flags and path are 0. Only the calling process's pid is accepted (`ESRCH` otherwise, -1 included, as FreeBSD's `pget` gives); a write returns `EPERM` and a short buffer `ENOMEM`. Ranges outside the arena, which guest code cannot map, are not reported. The walk takes the arena lock once per range, not for the whole walk as FreeBSD holds the map lock, so an `mmap` or `munmap` on another thread can change the list between two records. Whether a PS5 application may read its own map, and what types and paths the console reports, is unverified +- [sysctl names a PS5 payload process gets ENOENT for](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` returns -1 with `ENOENT` for `hw.byteorder`, `hw.cacheline_size`, `hw.machine_arch`, `hw.usermem`, `kern.clockrate`, `kern.maxfiles`, `kern.maxfilesperproc`, `kern.maxproc`, `kern.ps_strings`, `kern.sched.name`, `kern.smp.maxcpus`, `kern.stackprot`, `kern.timecounter.hardware` and `machdep.cacheline_size`, and `sysctl` does the same for the MIBs {1, 6}, {1, 7}, {1, 12}, {1, 27}, {1, 32}, {6, 4} and {6, 11}. The buffer and the length are left alone. A payload process on a PS5 (firmware 12.70) gets this for a read, a size query, a null length and a null buffer with a null length, by name and by MIB. `sysctlnametomib` gives those seven MIBs, and returns `ENOENT` for `hw.cacheline_size`, `hw.usermem` and `machdep.cacheline_size` without touching the count or the first three words of the MIB, as the console does. A write to these names and MIBs throws, except for `machdep.cacheline_size`, which returns `ENOENT` as FreeBSD amd64 does for an OID it does not define; the console's answer to a write was not tried. The console's `sysctlnametomib` gives `kern.smp.maxcpus`, `kern.stackprot`, `kern.sched.name` and `kern.timecounter.hardware` the MIBs {1, 2147482816, 2147482814}, {1, 2147483151}, {1, 2147483182, 2147483181} and {1, 2147482838, 2147482834}, and its `sysctl` returns `ENOENT` for them; here `sysctlnametomib` throws for them. `hw.physmem`, `hw.realmem` and `vm.overcommit` return `ENOENT` on the console too but report values here, as the `hw.realmem` and `vm.overcommit` entries say. A payload process also gets `ENOENT` for `kern.anyps5_no_such_name`, which its `sysctlnametomib` does not resolve; other names still throw here. A title process was not measured - [hw.pagesize](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports the system page size (16384 bytes, PS5_PAGE_SIZE) as an integer through sysctl MIB {6, 7} (CTL_HW, HW_PAGESIZE) and the "hw.pagesize" sysctlbyname lookup, following FreeBSD's [`hw.pagesize`](https://github.com/freebsd/freebsd-src/blob/main/sys/kern/kern_mib.c#L177), a read-only `SYSCTL_INT` of `PAGE_SIZE`. The value on a PS5 follows the 16 KiB page size the rest of libkernel uses and is unmeasured. A write returns -1 with EPERM. - [hw.model](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports the [host CPU's name](../../../core/libs/prx/libkernel/System/src/HostCpu.cpp), built as FreeBSD 11 builds `hw.model`: the CPUID brand string (leaves 0x80000002 to 0x80000004) without leading spaces ([identcpu.c](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/x86/x86/identcpu.c#L683-L691)). Trailing padding stays, as it does there, so an AMD host's name ends in spaces. The name a PS5 reports is unknown. A write returns -1 with `EPERM`. A host without an x86 CPUID brand string, such as an Apple Silicon Mac, throws. PPSA99203 only prints it (`[console] hw.model: %s`) - [Host CPU check](../../../core/libs/prx/libkernel/System/src/HostCpu.cpp) (libkernel) - when libkernel loads on a Linux or Windows x86-64 host, it logs which of the PS5 CPU's user-mode extensions CPUID reports and throws, naming the host CPU, if any extension is missing that LLVM selects from plain C/C++ for the PS5's Zen 2 (`-march=znver2`, [feature list](https://github.com/llvm/llvm-project/blob/llvmorg-19.1.7/llvm/lib/Target/X86/X86.td#L1459-L1513)): SSE to SSE4.2, SSSE3, POPCNT, LZCNT, BMI1, BMI2, MOVBE, CMPXCHG16B, LAHF/SAHF, AVX, AVX2, FMA, F16C, and the operating system's AVX state in XCR0 (#3719). SSE4A, CLZERO and SHA, which `--to-intel` converts, and AES, PCLMULQDQ, RDRAND, RDSEED, ADX, CLFLUSHOPT, CLWB, RDPID, XSAVEOPT, XSAVEC and PREFETCHW, which compilers emit only for intrinsics or explicit prefetches, are logged but not required: a title that executes one of them on a host without it still stops with an illegal instruction. macOS hosts are not checked: some Rosetta versions execute extensions without reporting them through CPUID, but support differs by host. ANDN and RDRAND fault on an M1 with macOS 15.3.1; see the [Rosetta conversion limits](relinker.md) - [machdep.tsc_freq](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports the host TSC frequency that `sceKernelGetTscFrequency` measures, as a 64-bit value like FreeBSD 11's [`sysctl_machdep_tsc_freq`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/x86/x86/tsc.c#L658-L676). FreeBSD lets root write it to change the TSC timecounter's frequency; a write here returns -1 with `EPERM`. PPSA99203 reads it at start-up and measures the TSC itself when the value is 100 MHz or less -- [getrlimit](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - `RLIMIT_CPU`, `RLIMIT_FSIZE`, `RLIMIT_CORE`, `RLIMIT_SBSIZE`, `RLIMIT_VMEM`/`AS`, `RLIMIT_NPTS`, `RLIMIT_SWAP`, `RLIMIT_KQUEUES` and `RLIMIT_UMTXP` report infinity as in FreeBSD's process-0 defaults, and `RLIMIT_DATA` mirrors the host limit. `RLIMIT_NOFILE` reports, as both soft and hard limit, the number of descriptors after which AnyPS5 itself fails with `EMFILE`: on Windows the C runtime's 8192 low-level descriptors, which guest files, pipes and sockets all take, and as many stdio streams, which libkernel raises from 512 at load with [`_setmaxstdio`](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/setmaxstdio), and on Linux the host's `RLIMIT_NOFILE`, whose soft limit libkernel raises to the hard limit at load. The console's limit is not known; XPSemu (PPSA97358) reads it in QEMU's `os_setup_limits` and raises the soft limit to the hard one when they differ. `RLIMIT_STACK`, `RLIMIT_RSS`, `RLIMIT_MEMLOCK` and `RLIMIT_NPROC` throw because no console values are established. -- [setrlimit](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) validates the resource and input in FreeBSD order, maps negative limits to infinity, and lowers a soft limit above the hard limit. A request succeeds when its normalized values match `getrlimit`, or for `RLIMIT_NOFILE` when `rlim_max` does not exceed the current hard limit and `rlim_cur` does not exceed `rlim_max`, storing the updated limits for subsequent `getrlimit` queries; descriptor allocation enforcement of the lowered soft limit is not implemented, and changing other limits throws because guest limit state is not stored. Java 25 LAN / DNS (PPSA99869) sets `rlim_cur = MIN(0x100000, rlim_max)` during VM initialization +- [getrlimit](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - `RLIMIT_CPU`, `RLIMIT_FSIZE`, `RLIMIT_CORE`, `RLIMIT_SBSIZE`, `RLIMIT_VMEM`/`AS`, `RLIMIT_NPTS`, `RLIMIT_SWAP`, `RLIMIT_KQUEUES` and `RLIMIT_UMTXP` report infinity as in FreeBSD's process-0 defaults, and `RLIMIT_DATA` mirrors the host limit; a payload process on a PS5 (firmware 12.70) reads 0x800000000 for both limits. `RLIMIT_NOFILE` reports, as both soft and hard limit, the number of descriptors after which AnyPS5 itself fails with `EMFILE`: on Windows the C runtime's 8192 low-level descriptors, which guest files, pipes and sockets all take, and as many stdio streams, which libkernel raises from 512 at load with [`_setmaxstdio`](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/setmaxstdio), on Linux the host's `RLIMIT_NOFILE`, whose soft limit libkernel raises to the hard limit at load, and on macOS the smaller of the host's hard limit and `kern.maxfilesperproc`, which libkernel sets as both limits at load, since macOS fails with `EMFILE` at `kern.maxfilesperproc` whatever the soft limit. A payload process on a PS5 (firmware 12.70) reads 13952 for both limits, while `getdtablesize` returns 12555 and the process can open descriptors up to 12554. XPSemu (PPSA97358) reads `RLIMIT_NOFILE` in QEMU's `os_setup_limits` and raises the soft limit to the hard one when they differ. `RLIMIT_STACK` (0x200000), `RLIMIT_RSS` and `RLIMIT_MEMLOCK` (infinity) and `RLIMIT_NPROC` (1604) report the soft and hard limits a payload process reads on a PS5 (firmware 12.70). Guest locking still follows the host: `mlock` and `sceKernelMlock` fail at the host's hard `RLIMIT_MEMLOCK` on Linux, and on Windows when the process working set cannot grow, although `RLIMIT_MEMLOCK` reports infinity. There the main thread's stack is 0x200000 as well (`pthread_attr_get_np`); here the main thread runs on the host thread's stack, whose size can differ. Whether a title process reads the same limits is unverified. +- [setrlimit](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) validates the resource and input in FreeBSD order, maps negative limits to infinity, and lowers a soft limit above the hard limit. A request other than `RLIMIT_NPROC` succeeds when its normalized values match `getrlimit`, or, for `RLIMIT_NOFILE`, when `rlim_max` does not exceed the current hard limit and `rlim_cur` does not exceed `rlim_max`, storing the updated limits for subsequent `getrlimit` queries; `RLIMIT_NPROC` always throws; descriptor allocation enforcement of the lowered soft limit is not implemented, and changing other limits throws because guest limit state is not stored. In a payload process on a PS5 (firmware 12.70), `setrlimit(RLIMIT_NOFILE)` lowers a requested 13952 to 12555, as the hard and as the soft limit; here a request within the current hard limit is stored as given and one above it throws; setting `RLIMIT_STACK`, `RLIMIT_RSS` and `RLIMIT_MEMLOCK` to their current values succeeds and changes nothing, as here; setting `RLIMIT_NPROC` to its current 1604 also succeeds but leaves 1443, as FreeBSD 11 would if the console's `maxproc` were 1604: process 0's limit starts at `maxproc` ([`init_main.c`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/init_main.c#L550-L551)), `maxprocperuid` starts at nine tenths of it ([`subr_param.c`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/subr_param.c#L258)), and `setrlimit` lowers an `RLIMIT_NPROC` limit above `maxprocperuid` to it ([`kern_resource.c`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/kern_resource.c#L714-L718)); 1604 × 9 / 10 gives 1443. Neither value is measured: `kern.maxproc` fails with `ENOENT` in the payload process. Here that call throws, because the limit is not stored. Java 25 LAN / DNS (PPSA99869) sets `rlim_cur = MIN(0x100000, rlim_max)` during VM initialization - [execvp](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws: replacing the process image is not supported. PPSA85300 imports it and fails to load without it; its `fork` and `vfork` fail with `ENOSYS` ([libc_missing.c](https://github.com/ZiZc3/VLC/blob/f3253020a64368153233ede29183014c0b6c8979/ps5/compat/libc_missing.c#L290-L292)), so the child process that would call it is never created. - [nmount](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) throws: mounting file systems is not supported. Prospero Themes (PPSA90210) imports it and fails to load without it; it opens its theme library without calling it, and its strings (`/dev/ssd0.system_ex`, `/system_ex`, `exfatfs`, `fstype`, `fspath`, `from`, `ignoreacl`) point to remounting the console's `/system_ex` partition to write theme files there. - [socketpair](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - the pair is the host's `AF_UNIX` pair on Linux and, 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. -- [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 +- [recvmsg](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) loses received stream data on an output copy fault: outputs are checked after the host receive. FreeBSD 11.4 leaves unread stream data queued, though it also drops a datagram on a copy fault ([`soreceive_dgram`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L2308-L2312), [`soreceive_generic`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L1758-L1774)). A Linux `process_vm_readv` or `process_vm_writev` failure other than `EFAULT` throws +- [MSG_DONTWAIT](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) in `send`, `sendto`, `sendmsg`, `recv`, `recvfrom` and `recvmsg` makes that one call non-blocking through the host's `MSG_DONTWAIT`, as FreeBSD 11.4's `soreceive_generic` returns `EWOULDBLOCK` for it (`sys/kern/uipc_socket.c`). Winsock has no per-call non-blocking flag, so on Windows the call first asks `WSAPoll` with a zero timeout whether the socket is ready and fails with `EAGAIN` when it is not. The check and the transfer are two host calls: a second thread that takes the data in between can leave a `recv` blocked, and a `send` of more bytes than the socket buffer holds can still block until it has sent them, where FreeBSD's `sosend_generic` returns a partial count - [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 - [poll](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) waits on guest sockets and pipes, through `WSAPoll` on Windows and `poll` on Linux; the `pollfd` layout and flag values are FreeBSD's. A libSceNet socket joins the same host wait through the native socket libSceNet lends to libkernel. `WSAPoll` rejects `POLLPRI`, so on Windows a socket's `POLLPRI` is asked as `POLLRDBAND` and `POLLIN` as `POLLRDNORM`; Winsock reports urgent data until it is read with `MSG_OOB`, where FreeBSD 11.4's `sopoll_generic` stops once the normal stream passes the mark. Guest `send` and `recv` refuse `MSG_OOB` with `EOPNOTSUPP`, so urgent data can only come from an outside peer and Windows keeps reporting `POLLPRI` after the stream passes the mark. A pipe follows FreeBSD's `pipe_poll`: the read end is readable when data is buffered, either end reports `POLLHUP` once the other end is closed, with the requested `POLLIN` and `POLLRDNORM` bits and without `POLLOUT`, which `pollscan` clears, and the write end is writable when `PIPE_BUF` (512) bytes are free on Windows, or when the host kernel says so on Linux. `POLLOUT` on a read end throws: FreeBSD opens both ends for reading and writing and reports it writable, and host pipes are one-way. Windows pipes cannot be waited on, so while a pipe is polled `poll` checks it with `NtQueryInformationFile` and waits on the sockets in slices of 1 ms. A regular file, a directory or a file read from app0.zar is always ready for `POLLIN`, `POLLRDNORM` and `POLLOUT` and never for `POLLPRI`, `POLLRDBAND` or `POLLWRBAND`, as FreeBSD 11.4's [`vop_stdpoll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_default.c#L563-L574) and `vop_nopoll` answer every event `poll` accepts; which of the two the console's filesystems use is not known, and they agree on these events. `/dev/random` and `/dev/urandom` follow FreeBSD 11.4's [`randomdev_poll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/dev/random/randomdev.c#L274-L285) for a seeded device: a request with `POLLIN` or `POLLRDNORM` gets only those bits back, and any other request gets every requested bit. Open descriptors 0 to 2 follow the console model `select` uses: descriptor 0 never reports an event and descriptors 1 and 2 report `POLLOUT`, whatever the host attaches to them. A closed descriptor reports `POLLNVAL`, and an open descriptor that is neither a guest socket, a libSceNet socket, a pipe, a file, a directory, the random device nor descriptor 0 to 2 (other devices, a kqueue) throws, as do event bits other than `POLLIN`, `POLLPRI`, `POLLOUT`, `POLLRDNORM`, `POLLRDBAND` and `POLLWRBAND` (the `POLLERR`, `POLLHUP` and `POLLNVAL` bits are ignored in `events`), an infinite timeout with no socket or pipe to wait on. A closed socket descriptor reports `POLLNVAL`; the console's limit on the descriptor count is unknown and not checked -- [kevent on a pipe](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - `EVFILT_READ` on the read end of a pipe follows FreeBSD 11's [`filt_piperead`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L1787-L1806): `data` is the number of buffered bytes, and `EV_EOF` is set once the write end is closed. Windows pipes cannot be waited on, so while a kqueue waits on a pipe it checks it with `NtQueryInformationFile` and waits on its sockets in slices of 1 ms; a wait with no pipe is not sliced. `EVFILT_READ` on the write end of a pipe throws: FreeBSD's `pipe` opens both ends for reading and writing ([`kern_pipe`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L422)), and host pipes are one-way. `EVFILT_WRITE` on a pipe throws too, and `NOTE_LOWAT` on a pipe is ignored, as `filt_piperead` ignores it. +- [kevent EV_CLEAR on a socket or a pipe](../../../core/libs/prx/libkernel/Equeue/src/EdgeWatch.cpp) (libkernel) - FreeBSD 11 activates an `EV_CLEAR` knote when `kqueue_register` finds it ready, which every `EV_ADD` or `EV_ENABLE` of an enabled knote does again, and on each `KNOTE` the socket or pipe raises (`sorwakeup`, `sowwakeup`, `pipeselwakeup`); [`kqueue_scan`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_event.c) checks the filter again before reporting, drops a knote that is no longer ready and deactivates a reported one until the next activation. On Linux each such knote registers the host socket or pipe with its own epoll instance in edge-triggered mode (`EPOLLIN | EPOLLRDHUP` for `EVFILT_READ`, `EPOLLOUT` for `EVFILT_WRITE`), and a wakeup of the host descriptor with that mask stands for a `KNOTE`: the knote is then checked with the same readiness test as a level-triggered one, `NOTE_LOWAT` mark included, and a wait blocks on the epoll descriptor instead of the socket, so a socket that stays readable does not wake it. The host's wakeups are not FreeBSD's: FreeBSD raises `sowwakeup` whenever send buffer space is freed, while Linux TCP wakes `EPOLLOUT` waiters only once the socket is writable again after a write found it full (`SOCK_NOSPACE`, [`tcp_check_space`](https://github.com/torvalds/linux/blob/v6.8/net/ipv4/tcp_input.c#L5639)), so a TCP writer that never fills its buffer sees fewer `EVFILT_WRITE` events than on FreeBSD; a host stack may also coalesce data wakeups, so the number of times a busy socket fires can differ. Windows and macOS throw, as the host there has no edge-triggered wait over these descriptors that is wired up; `EV_CLEAR` on the timer, user and vnode filters does not use this and works everywhere. +- [kevent on a pipe](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - host pipes are one-way, where FreeBSD's [`kern_pipe`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L422) opens both ends for reading and writing, so `EVFILT_READ` on the write end and `EVFILT_WRITE` on the read end throw; on macOS `EVFILT_WRITE` on a pipe throws, as the host has no call for a pipe's capacity. `EVFILT_WRITE` reports the free space of the host buffer, not of FreeBSD's 16 KiB one, and on Linux fires when the host says the pipe is writable rather than at `PIPE_BUF` (512) free bytes. Windows pipes cannot be waited on, so a kqueue waiting on one checks it every 1 ms. Otherwise [`filt_piperead`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L1787-L1828) and `filt_pipewrite` are followed: `EV_EOF` once the other end closes, `EPIPE` for a write event added after the read end closed, and `NOTE_LOWAT` ignored. On Windows `fcntl` cannot make a pipe write end non-blocking, so the full-pipe and drain case of `EVFILT_WRITE` is tested on Linux only and the `NtQueryInformationFile` write quota is checked there only for the free space, `EV_EOF` and `EPIPE`. - [Socket error numbers](../../../core/libs/prx/libkernel/Socket/include/SocketError.hpp) (libkernel) - a host socket error becomes the FreeBSD 11 `errno.h` number of the same name; the numbers are not checked on the console. Winsock `WSAESHUTDOWN` becomes `EPIPE`, as a send after `shutdown` does on FreeBSD, and a host error with no FreeBSD socket counterpart becomes `EIO`. A nonblocking `connect` in progress reports `EINPROGRESS` on Linux but `EAGAIN` on Windows (`WSAEWOULDBLOCK`), where FreeBSD reports `EINPROGRESS`. On Windows a refused nonblocking `connect` is never reported (`SO_ERROR` is unsupported), so a repeated one keeps reporting `EALREADY` where FreeBSD reports `ECONNREFUSED`. A receive after `shutdown(SHUT_RD)` reports `EPIPE` on Windows where FreeBSD returns 0 - [getsockopt, setsockopt](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) on a guest socket handle the SOL_SOCKET level, `TCP_NODELAY` and `IPV6_V6ONLY`, after FreeBSD 11.4's [`sosetopt` and `sogetopt`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c): `SO_REUSEADDR`, `SO_KEEPALIVE`, `SO_DONTROUTE`, `SO_BROADCAST`, `SO_OOBINLINE` and `SO_LINGER` act on the host socket, and `SO_ERROR`, `SO_TYPE`, `SO_ACCEPTCONN`, `SO_SNDBUF` and `SO_RCVBUF` are read. A boolean option reads back as its own bit, as FreeBSD returns `so_options & option`. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `connect`; a second failure with the same code on the same socket without a `connect` in between would be hidden. `SO_SNDBUF` and `SO_RCVBUF` read the host's value, which is not the size FreeBSD reports (Linux doubles it). `SO_NOSIGPIPE` is kept per socket and reads back as its bit. On Linux a `send`, `sendto`, `sendmsg` or `write` on that socket passes `MSG_NOSIGNAL` to the host, as [`kern_sendit`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L890-L895) and [`soo_write`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_socket.c#L147-L151) leave out the signal. An accepted socket takes it from its listener, as [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L603) copies the options. `SO_NBIO` (`0x1200`) is the console's own option, not FreeBSD's. It switches the socket between blocking and non-blocking as `FIONBIO` does and reads back as 0 or 1, as `sceNetSetsockopt` and `sceNetGetsockopt` treat it here; what the console reads back for it is not measured. `TCP_NODELAY` (`IPPROTO_TCP`, 1) is set on the host socket and reads back as `TF_NODELAY` (4), as [`tcp_default_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/tcp_usrreq.c#L1860-L1861) returns it. The `IPPROTO_TCP` level on a datagram or `AF_UNIX` socket fails with `EINVAL`, as [`ip_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/ip_output.c#L943-L944) and [`uipc_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_usrreq.c#L1228-L1229) answer it, and an accepted socket takes `TCP_NODELAY` through the host. `SO_SNDTIMEO` and `SO_RCVTIMEO` follow [`sosetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2657-L2689) and [`sogetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2846-L2850). A 16-byte `timeval` with a negative field or 1000000 microseconds or more fails with `EDOM`, and more than `INT32_MAX` seconds becomes the longest wait. The value is kept and read back through FreeBSD's 32.32 fixed-point conversion ([`tvtosbt`, `sbttotv`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/sys/time.h#L178-L286)), so half a second reads back as 499999 microseconds and one microsecond as 0. A `recv`, `recvfrom`, `recvmsg`, `read` or `readv`, and a `send`, `sendto`, `sendmsg`, `write` or `writev`, on a blocking socket then waits that long for the host to report the socket readable or writable, and fails with `EWOULDBLOCK`; a call with `MSG_DONTWAIT` does not wait. An accepted socket takes both from its listener ([`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L629-L630)). The wait covers only the first transfer: once the socket is writable the host send runs without a limit, where FreeBSD stops a send that cannot finish and returns the bytes sent so far, and a receive whose data another thread takes between the wait and the host call blocks again. `sendfile` ignores the send timeout. The host wait has millisecond steps, so a shorter timeout lasts up to a millisecond longer. A null value fails with `EINVAL`, where FreeBSD's [`kern_setsockopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L1414-L1415) returns `EFAULT`. `IPV6_V6ONLY` (`IPPROTO_IPV6`, 27) on an `AF_INET6` socket is set on the host socket and reads back as 0 or 1, after FreeBSD's [`ip6_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L1716-L1732): a value that is not exactly an `int` fails with `EINVAL` ([L1572](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L1572)), as does setting it on a bound socket, which the host refuses too. `socket` turns it on for every `AF_INET6` socket, as FreeBSD's default `net.inet6.ip6.v6only` does, and Java turns it off on the sockets it opens: Java 25 Advanced Network (PPSA99871) stops at `java.net.SocketException: Protocol not available` without it. The `IPPROTO_IPV6` level on an `AF_INET` or `AF_UNIX` socket fails with `EINVAL`, as `ip_ctloutput` and `uipc_ctloutput` answer it. Every other SOL_SOCKET option (`SO_DEBUG`, `SO_USELOOPBACK`, `SO_REUSEPORT` and `SO_TIMESTAMP`, which FreeBSD stores, the low-water marks among them; PPSA99871 sets `SO_REUSEPORT` and carries on when it fails), every other `IPPROTO_TCP` option (`TCP_MAXSEG`, `TCP_KEEPIDLE`, `TCP_KEEPINTVL` and `TCP_KEEPCNT` among them), every other `IPPROTO_IPV6` option ([L1928-L1929](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L1928-L1929), [L2158-L2159](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L2158-L2159)) and the `IPPROTO_IP` level fail with `ENOPROTOOPT`. None of this was measured on a console. - [connect to AF_UNSPEC](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) dissolves a datagram association as FreeBSD 11.4's `udp_disconnect()` does, which resets the local address to the wildcard address and keeps the port. On Linux a socket bound to an explicit port keeps its local address as well (`127.0.0.1:` stays `127.0.0.1:`), because a bound socket cannot be bound again; one bound to port 0 is bound again to the wildcard address with the port it had @@ -193,13 +210,14 @@ - [sigaction](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records the handler, flags and mask of every signal from 1 to 128 and reports them back, following FreeBSD's `sigaction` (`EINVAL` for other signals, and for a handler on `SIGKILL` or `SIGSTOP`); `signal` records its handler with `SA_RESTART`, as FreeBSD's libc does. Only handlers without `SA_SIGINFO` on the signals `signal` already delivers (`SIGINT`, `SIGILL`, `SIGABRT`, `SIGFPE`, `SIGSEGV`, `SIGTERM`) run, the same way `signal` handlers do; their mask and flags are not applied. `SA_SIGINFO` handlers are recorded but never called, since that needs FreeBSD's `siginfo_t` and `ucontext_t`, and neither are the handlers of other signals (`SIGBUS`, `SIGHUP`, ...), whether they are set by `sigaction` or by `signal`, which records them too (every number from 1 to 128, with `SA_RESTART` and an empty mask replacing what `sigaction` stored; `SIGKILL` and `SIGSTOP` accept only the default handler in both calls, as in FreeBSD's `kern_sigaction`). `raise` and `kill` accept every number from 1 to 128 now: a blocked one stays pending on the set of `sigtimedwait`, an unblocked one whose handler is registered runs it on the calling thread, and an unblocked one that has no host equivalent and no handler still fails with `EINVAL`, where FreeBSD runs its default action. `raise` of an `SA_SIGINFO` handler still throws instead of terminating - [kill, killpg](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follow FreeBSD's `kern_kill` and `killpg1` for the only guest process: `EINVAL` for signals outside 0 to 128 and for negative `killpg` process groups, signal 0 only checks that the target exists, and its own pid, 0 and minus its own pid (the guest is taken to lead its own process group) signal it through `raise`, so they share `raise`'s limits above. Any other pid, and -1, which skips the caller, report `ESRCH`, so `EPERM` is never reported - [sceKernelFcntl](../../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) runs the POSIX `fcntl` below and returns its guest errno as `SCE_KERNEL_ERROR_*`, the form the other `sceKernel` file exports use. The return values and error codes are unverified on a console -- [fcntl](../../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) follows FreeBSD 11.4's [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L479-L799) and [`kern_dup`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L812) on file, directory, pipe and character device descriptors; a socket keeps the libSceNet path (`F_GETFL`, `F_SETFL` with `O_NONBLOCK`, `F_GETFD` and `F_SETFD`, `EINVAL` for every other command, see the next entry). 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 +- [fcntl](../../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) follows FreeBSD 11.4's [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L479-L799) and [`kern_dup`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L812) on file, directory, pipe and character device descriptors; a socket keeps the libSceNet path (`F_DUPFD`, `F_DUP2FD`, `F_DUPFD_CLOEXEC`, `F_DUP2FD_CLOEXEC`, `F_GETFL`, `F_SETFL` with `O_NONBLOCK`, `F_GETFD` and `F_SETFD`, `EINVAL` for every other command, see the next entry). On Linux and macOS a host socket that is not a guest socket, such as a standard descriptor a service manager connected to its journal, takes the flag handling of a pipe end and `FIONREAD` asks the host for its queued bytes (for a datagram socket Linux counts only the next datagram, where FreeBSD's [`soo_ioctl`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_socket.c#L202-L205) counts every buffered byte); its record locks and read-ahead hints fail with `EBADF`, as FreeBSD fails them on a descriptor without a vnode, and `O_ASYNC` on it throws. The close-on-exec flag and the status flags live in a guest table, the status flags shared by `dup`, `dup2` and `F_DUPFD` as in an open file description; a descriptor the table has not seen, such as 0 to 2, starts from the host's flags, since the console's are not known. `F_GETFL` takes the access mode from the host (the granted access of the handle on Windows), so pipe ends report `O_RDONLY` and `O_WRONLY` where FreeBSD's bidirectional pipes report `O_RDWR`. `F_DUPFD` and `F_DUPFD_CLOEXEC` return the lowest free host descriptor at or above the start, so the bound is the host table (`EMFILE` on Windows, `EINVAL` past `RLIMIT_NOFILE` on Linux), not the console's unknown `RLIMIT_NOFILE` and `kern.maxfilesperproc`. `O_NONBLOCK` switches a host pipe (`PIPE_NOWAIT` on Windows, where an empty read and a full write then report `EAGAIN`) and is only recorded on files, directories and the random device, as their reads never block; on a Windows console or other character device it throws. Changing `O_APPEND` on a Windows file, changing `O_FSYNC` on a file, `O_ASYNC` on a pipe or character device (`SIGIO` is not delivered), `F_GETOWN`, `F_SETOWN` and `F_SETLK_REMOTE` throw; `O_DIRECT`, `F_RDAHEAD` and `F_READAHEAD` are recorded as the hints they are. Record locks (`F_GETLK`, `F_SETLK`, `F_SETLKW` and their forms without `l_sysid`) use the host's POSIX locks on Linux and macOS. `F_GETLK` with `F_UNLCK` asks the host with `F_RDLCK`: as in [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) only another owner's overlapping write lock conflicts with either request. On Windows they follow [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L607-L752) and [`lf_advlockasync`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L433-L487) for the one guest process, which owns every lock: `EOVERFLOW` for an `SEEK_CUR` start past `OFF_MAX`, then `EBADF` for `F_RDLCK` without read access or `F_WRLCK` without write access and `EINVAL` for another type, then `EINVAL` and `EOVERFLOW` for the range; as [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) never lets locks of one owner conflict, whichever descriptor or thread asks, `F_SETLK` and `F_SETLKW` then succeed without waiting and `F_GETLK` reports `F_UNLCK`, so no lock list is kept and merging, splitting and the release on close have nothing to change. Nothing is held on the host, since Windows byte-range locks are mandatory and would fail the guest's own reads and writes through other descriptors, so other host processes neither see these locks nor are seen; a lock from the end of a directory throws. The return values and error codes are unverified on a console +- [fcntl on a guest socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) answers `F_DUPFD`, `F_DUP2FD`, `F_DUPFD_CLOEXEC`, `F_DUP2FD_CLOEXEC`, `F_GETFD`, `F_SETFD`, `F_GETFL` and `F_SETFL` and fails every other command with `EINVAL`; FreeBSD 11.4's `kern_fcntl` also handles `F_GETOWN` and `F_SETOWN` on a socket. `FD_CLOEXEC` is only recorded (there is no `exec`): `dup`, `dup2`, `F_DUPFD`, `F_DUP2FD` onto another descriptor and an accepted socket start without it, `F_DUP2FD` onto the same descriptor keeps it, and `SOCK_CLOEXEC`, `F_DUPFD_CLOEXEC`, `F_DUP2FD_CLOEXEC` and `F_SETFD` set it. None of this was measured on a console. +- [ioctl, _ioctl](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) handle only `FIONBIO` and `FIONREAD` on a socket, and any other request there fails with `ENOTTY`. On the other descriptors they follow FreeBSD's [`kern_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_generic.c#L758), [`vn_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_vnops.c#L1488) and [`pipe_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_pipe.c#L1346): `FIONBIO` sets `O_NONBLOCK` as `fcntl` does, `FIOCLEX` and `FIONCLEX` set and clear the close-on-exec flag `F_GETFD` reports, and `FIONREAD` reports the size past the offset of a file, the queued bytes of a pipe's read end, 0 on its write end and `ENOTTY` on the random device, while it throws on a directory or another character device. Every termios request (group `t`, `TIOCGWINSZ` included) fails with `ENOTTY`, since those descriptors are taken as not a terminal, as in `tcgetattr`, and any other well-formed request throws. An input request with length 0 is not rejected as malformed: [`sys_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_generic.c#L703-L718) rejects it only in a kernel built without `COMPAT_FREEBSD4`, `COMPAT_FREEBSD5` and `COMPAT_43`, and which of these the PS5 kernel has is unknown. Stremio Plus (PPSA74126) imports `_ioctl` for its statically linked `tc*` functions, which send `TIOCGETA`, `TIOCSETA`, `TIOCSPGRP` and the other termios requests - [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 +- [pthread_kill](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) returns the error number like FreeBSD's `pthread_kill`: `EINVAL` for signals outside 0 to 128 and `ESRCH` for a null thread. A signal to the calling thread goes through `raise`, so it shares `raise`'s limits; a signal to another thread is left on the pending set for whichever thread waits for or unblocks it, since it is not delivered to the chosen thread, and a handle that is not a live guest thread returns `ESRCH` without touching it - [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) validates `tv_usec` but saturates a valid timeout beyond the remaining `steady_clock` range at `time_point::max()`, so the wait can continue effectively indefinitely. The console's behavior for a valid timeout outside the host clock's range is unknown. +- [inet_ntop, inet_pton](../../../core/libs/prx/libkernel/Socket/src/AddressConversion.cpp) (libkernel) - an `AF_INET6` result of `inet_ntop` is followed by zero bytes up to `size`. In a payload process on a PS5 (firmware 12.70), the text forms, the order of the checks and this padding were measured through both exports. The padding was sampled at sizes from 3 to 65536, above 64 only at 65, 128, 256, 1024, 4096 and 65536; the sizes that were not sampled, sizes above 65536 and a title process are assumed to behave the same - [_fstatfs](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) fills the FreeBSD 11 `struct statfs` of the PS5 payload SDK with the host volume that holds the file: block size, transfer size, total, free and available blocks, maximum name length and the read-only, `nosuid` and `noexec` flags (on Windows only read-only, and `f_files` and `f_ffree` are 0, as NTFS has no fixed number of files). The filesystem type number and name, mount paths, owner, filesystem id and I/O counters are left 0 or empty, since they would describe a host mount, not a console one. Sockets and pipes return `EINVAL`, as FreeBSD's `getvnode` does; a Windows descriptor that is neither a file nor a pipe throws. Stremio Plus (PPSA74126) reads the free space before a download and only logs the other fields - [pthread_spin_lock](../../../core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp) (libkernel) - a lock taken again by the thread that holds it throws here. On a PS5 (firmware 12.70) that call had not returned after 2 s, as FreeBSD 11.4 libthr [`thr_pspinlock.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_pspinlock.c) spins forever - [pthread_rwlockattr_getpshared, pthread_rwlockattr_setpshared](../../../core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp) (libkernel) - `pthread_rwlockattr_destroy` clears the attribute here, so a later get, or a set of 0, throws. On a PS5 (firmware 12.70) destroy leaves the attribute set; a get afterwards returned 0 with a stale value and a set of 0 returned 0 @@ -209,9 +227,14 @@ - [pthread_atfork](../../../core/libs/prx/libkernel/Pthread/Posix/Atfork.cpp) (libkernel) appends the three handlers to a process-wide list and returns 0, or `ENOMEM` when the entry cannot be allocated, as FreeBSD 11.4's [`_pthread_atfork`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_fork.c#L78-L100) does; null handlers are accepted. The handlers never run: `fork` throws and `vfork` is absent, so no child process is created. Handlers of a module that is unloaded stay in the list, where FreeBSD's `__pthread_cxa_finalize` removes them. PPSA97358 has a source-level call from the constructor `rcu_init` ([rcu.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/rcu.c#L488-L495)), but current-main runtime reachability is unverified; XPSemu currently stops first at the unresolved `mlockall` import (#4386) - [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 +- [Short sleeps ended by GPU and flip completions](../../../core/libs/prx/libkernel/Time/SleepWake.cpp) (libkernel) - a `sceKernelUsleep`, `usleep`, `nanosleep` or `sceKernelNanosleep` of 1 ms or less returns before the requested time, which FreeBSD and the console do not do: it ends once the AGC driver reports a completed GPU batch or batch completion action, or VideoOut completes a flip, so a title that polls a GPU label or the flip status between sleeps does not sleep the rest. A title that paces frames with such a sleep can wake up to 1 ms early. A thread returns early at most 8 times in a run of sleeps; the run restarts when the thread submits AGC work or did not sleep the requested time since its last sleep, so a loop that counts sleeps to time out still sleeps all but 8 in full. `APS5_NO_SLEEP_WAKE=1` keeps every sleep in full - [chflags](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) follows FreeBSD 11.4's [`kern_chflagsat`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L2366-L2383) and UFS's [`ufs_setattr`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/ufs/ufs/ufs_vnops.c#L509-L556) for a root caller: the path lookup reports `EFAULT`, `ENOENT` and `ENOTDIR`, a flag outside the 17 UFS stores (`VNOVAL` included) `EOPNOTSUPP`, and `SF_SNAPSHOT` `EPERM`. Neither host keeps BSD file flags, so every file is taken to have none (`stat` reports `st_flags` 0): flags 0 succeed without touching the file, and any other flag throws, naming the flags. `UF_READONLY`, `UF_HIDDEN`, `UF_SYSTEM` and `UF_ARCHIVE` are FreeBSD's names for the Windows attributes but are not mapped to them: UFS stores them without enforcing them, `FILE_ATTRIBUTE_READONLY` already carries the owner write bit of `chmod` on Windows, and Linux has no equivalent a process can set without `CAP_LINUX_IMMUTABLE`. The console's filesystem types, `EROFS` on read-only mounts and the change-time update are not modelled +- [fchflags](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) has the gaps of `chflags`: a valid non-zero flag throws, and the console's filesystem types, `EROFS` and the change-time update are not modelled. Character devices, the random device, other host descriptors and files read from app0.zar throw, since their console filesystems and the privilege check for devices are not modelled. On Windows, anonymous shared memory objects are temporary files that are not told apart from regular files, so they are treated as files instead of reporting `EINVAL` as FreeBSD 11.4's [`getvnode`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L3850-L3877) does. - [getpriority](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) follows FreeBSD 11.4's [`sys_getpriority`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_resource.c#L93-L165) for the only guest process, which is taken to run at nice 0: `PRIO_PROCESS` and `PRIO_PGRP` with 0 or its own pid (the guest is taken to lead its own process group, see `kill`) and `PRIO_USER` with 0 or its user id 0 return 0, any other id fails with `ESRCH` and any other `which` with `EINVAL`. As in FreeBSD's libc, where `getpriority` is the bare system call, errno is left unchanged on success. The nice value of a title process on the console is unverified, and the host nice of the process or of the threads `APS5_THREAD_PRIORITY=high` raises is not reported. `setpriority` accepts the same target ids (any other id fails with `ESRCH`, any other `which` with `EINVAL`), clamps the value to FreeBSD's `PRIO_MIN`-`PRIO_MAX` (-20 to 20) and stores it as the guest process's nice value, which `getpriority` returns; the host scheduling class is not changed, and whether a title process may lower the value without privilege is unverified - [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 +- [sceKernelLoadStartModule](../../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) loads a system library named by its console path, `/system/common/lib/.sprx` or the sandboxed `//common/lib/.sprx` that `sceKernelGetFsSandboxRandomWord` names, from the host `libs/.prx`, as libSceSysmodule loads modules by ID; a host library has no `module_start`, so `res` is 0. Other system directories, such as `/system/priv/lib`, are not mapped. The OpenOrbis piglet sample loads `libScePigletv2VSH.sprx` and `libScePrecompiledShaders.sprx` this way - [param.sfo](../../../core/libs/prx/libkernel/AppMetadata/src/ParamSfoParser.cpp) (libkernel) is read when `param.json` is absent, for `TITLE`, `TITLE_ID`, `DOWNLOAD_DATA_SIZE` and `USER_DEFINED_PARAM_1` to `_4`, and mounts `/data` and `/download0` like `param.json`; the localized `TITLE_00` to `TITLE_30` are not selected by the console language. +- [sysctl MIBs for kern.smp.maxcpus, kern.stackprot, kern.sched.name and kern.timecounter.hardware](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - on a PS5 (firmware 12.70), a payload process resolves these names to {1, 2147482816, 2147482814}, {1, 2147483151}, {1, 2147483182, 2147483181} and {1, 2147482838, 2147482834}; `sysctl` returns -1 with `ENOENT` for reads and size queries by those exact MIBs, leaving the buffer and length unchanged, including when either or both output pointers are null. Writes were not measured and throw. `sysctlnametomib` still throws for these four names; #7541 implements the mappings for nine of the other names. +- [sceKernelGetProcessAbiType](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) throws: no measured value, prototype or documented consumer of the process ABI type was found, and PS5_MihawkOS (`mihawk-99/PS5_MihawkOS`, PPSA99170) is the only title known to import it. Returning a guessed constant would let a title branch on a value the console never answered with, so the declaration only resolves the import until the console value is measured diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index e8322c41b3..25499ae3d1 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -12,6 +12,7 @@ - [mkdirat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `mkdir`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. On Windows, `mode` is ignored. Console behaviour is unverified - [readlink](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) on Windows reads a symbolic link's target from its reparse data and returns it with `/` separators, only when the target is relative, as `symlink` creates them. A symbolic link with an absolute target returns its print name, or its substitute name when the print name is empty, without a `\??\` or `\\?\` prefix. A directory junction returns the same kind of name when it is a drive path (`X:\...`), using the print name when that is a drive path and the substitute name otherwise. A mounted volume, whose substitute name is `\??\Volume{GUID}\`, stays a throw, because the name left after the prefix is not a drive path. Any other reparse point throws too, where FreeBSD reports `EINVAL` for a file that is not a link - [readlinkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `readlink`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Console behaviour is unverified +- [fstatat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `stat`, or to `lstat` with `AT_SYMLINK_NOFOLLOW` (0x200). Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory, and flags other than `AT_SYMLINK_NOFOLLOW` return `EINVAL`. PPSA74126 imports it. Console behaviour is unverified - [unlinkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `rmdir` (with `AT_REMOVEDIR`, 0x800) or `unlink`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Removing a directory without `AT_REMOVEDIR` fails with `EPERM`, removing `.` or `..` with `AT_REMOVEDIR` fails with `EINVAL`, and flags other than `AT_REMOVEDIR` fail with `EINVAL`. Console behaviour is unverified - [renameat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `rename`. Any other relative path is joined to its descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to relative descriptors, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Console behaviour is unverified - [fork](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) throws: creating a child process is not supported. PPSA19111 imports it and fails to load without it. Its engine calls `fork` in `Posix_Daemonize`, only with `-daemonize`, and in `Platform_ShellExecute` before `execvp("xdg-open")`, which the main menu's Previews button reaches, so pressing Previews ends in this throw. [setsid](../../../core/libs/prx/libkernel_sys/Export.cpp) fails with `EPERM`, as FreeBSD's does for a process group leader, since the guest is taken to lead its own process group (see `kill`). [umask](../../../core/libs/prx/libkernel_sys/Export.cpp) keeps the guest's mask (initially 022, masked to 07777) and returns the previous one, Linux named shared-memory creation applies it, but ordinary files are still created with the host process's mask diff --git a/docs/dev/debt/relinker.md b/docs/dev/debt/relinker.md index d49cedbfcb..549e2ad959 100644 --- a/docs/dev/debt/relinker.md +++ b/docs/dev/debt/relinker.md @@ -4,26 +4,29 @@ - [Executable guest imports](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) - on Linux and macOS, a relocated executable import resolved to one guest provider requires that module and its dependencies at startup, even when it was discovered through `--module-dir` without a DT_NEEDED entry. Unreferenced symbols and no-op relocations do not require an otherwise deferred module. Console module-start ordering is unmeasured. -- Windows [input and output collision checks](../../../core/relinker/main.cpp) use temporary hard links when MinGW reports different permissions for two multiply linked files, as with `.elf` and `.exe` names. The comparison fails if either file is read-only or either parent directory refuses the temporary link, even when the files are distinct. - [Runtime-loaded modules](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) (`--module-dir`) that another module needs are initialized at startup with no start arguments, before the module that needs them is loaded; the console is assumed to start them when that module is loaded. - The executable file that [relinker](../../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) generates opens a console when launched, unless it is relinked with `--windows-gui`, which selects the GUI subsystem. A GUI build started from Explorer has no standard output or error handle; the [entry stub](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp) gives each missing one `anyps5.log` beside the executable, replacing the previous run's file (#3282). Errors about the executable path or about waiting for the previous LoadExec process are written before the log is opened and are lost when the standard error handle is missing. If the log cannot be created, output to the missing handles is lost. - [Guest import registries](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) - declared host imports of bundled modules are recorded with their module name and relocation target, even when another guest module exports the same stripped NID. No-op relocations are omitted. Registries describe import bindings, not implementation correctness or runtime reachability. - Converted Windows main executables [omit `DYNAMIC_BASE`](../../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) by default, not only with `--windows-fixed-base`: on the console the executable lies inside the 40-bit window that the title's own `sceLibcMspaceCreate` checks (#3043). Relocations are kept, so if `0x140000000` is taken Windows relocates the image, possibly above that window. Verified only on Windows 10. - [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. - A `PT_SCE_RELRO` segment that no `PT_LOAD` covers, the PS4 layout, is [loaded](../../../core/relinker/relinker/src/parsing/ElfReader.cpp) as a read-write `PT_LOAD` and stays writable after relocation; FreeBSD rtld makes `PT_GNU_RELRO` read-only once relocation is done, and what the console does with `PT_SCE_RELRO` is not checked. - 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. +- Read-only data can also sit between the unwind functions of such images. There, a `syscall`, `sysenter` or `int 0x80` pattern fails the [syscall scan](../../../core/relinker/relinker/src/analysis/SyscallScanner.cpp) only when a constant syscall number is set before it; the others, and every `sysret`, are skipped and counted in one message. The number is looked for in the decoded instructions and, whatever alignment the decoding took, in the bytes before the pattern: a `mov eax, imm32`, `mov rax, imm32`, `mov rax, imm64` or `xor eax, eax`, optionally followed by `mov al, imm8` or `mov ax, imm16`, then by any number of register moves or zeroings that leave `rax` alone. A real syscall without an unwind entry whose number is set otherwise is not refused and runs on the host: from memory or another register (`mov eax, ecx`), by `push`/`pop` or arithmetic (`or eax, imm32`), or with any other instruction between the load and the pattern. +- In the [code analysis](../../../core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp) of the Windows and macOS outputs for such images, a relocation or `lea` target outside every unwind function is followed as code only when its linear decoding, up to the first jump, return or trap, the next unwind function or 256 instructions, holds an `fs:[disp32]` access, or when it is 16-byte aligned, starts less than 256 bytes after an unwind function and ends in a jump, return or trap. Every other such target is taken as data and counted in one message, and the Windows TLS rewrite fails instead of overwriting one. A TLS access of another form, such as `mov rax, fs:[rax]`, or a `gs` instruction in code reached only through such a target is therefore not rewritten or trapped. Other fs and gs instructions do not tell code from data there: string bytes decode as these prefixes, and in two measured titles about 2,500 of about 5,500 such targets decode to one before their first jump. - 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`. +- In macOS output, a guest instruction that addresses memory through GS becomes `UD2` and NOP padding of its own length ([macOS image writer](../../../core/relinker/elfpatcher/src/macos/MacOsImage.cpp)): the host owns GS, which holds the pthread TSD base the guest TLS stubs read, and the guest GS base that FreeBSD's `sysarch(AMD64_SET_GSBASE)` would set is not emulated. A converted module runs until such an instruction is reached, which then faults. Prospero Win (PPSA99995) has six in its Wine `ntdll`. - The Linux placement of the `--to-intel` stubs in the [ELF patcher](../../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp) is covered only by a synthetic test. - `--to-rosetta` lowers what Rosetta lacks on top of `--to-intel` ([matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp)). AVX2, FMA, BMI1/2, F16C and MOVBE were observed to execute on an M5 Max without being reported through CPUID; this is not a guarantee for other Rosetta hosts. ANDN and RDRAND fault on an M1 running macOS 15.3.1 and execute on an M1 running macOS 26.6.2, so on the M1 the difference follows the macOS version; on that host BEXTR, BLSI, BLSMSK, BLSR, TZCNT, LZCNT, POPCNT, BZHI, MULX, PDEP, PEXT, RORX, SARX, SHLX, SHRX, MOVBE, FMA, AVX2 and F16C execute as well, its CPUID leaf 1 reports none of MOVBE, FMA, AVX or F16C, and RDSEED, RDPID, CLWB, ADCX and ADOX raise SIGILL. They also execute on an M5 running macOS 26.4, where the rest of BMI1 and BMI2, LZCNT, PREFETCHW, CLFLUSHOPT, AVX, AVX2, FMA, F16C and MOVBE execute as well although CPUID leaf 7 reads zero and leaves 1 and 0x80000001 report none of them, and where RDSEED, CLWB, RDPID, ADCX and ADOX raise an illegal instruction. ANDN is lowered, but RDRAND and other BMI instructions still depend on the host. RDSEED becomes RDRAND and CLWB a NOP. RDPID returns processor 0, and RDTSCP runs unchanged with ECX set to 0 after it, because Rosetta's TSC_AUX is the constant 0xAAAAAAAB (also measured on an M5, macOS 27.0.1). An RDPID or RDTSCP site with no movable instruction after it for the jump is kept; a kept RDTSCP reports 0xAAAAAAAB. ADCX/ADOX fault under Rosetta and are not converted: compilers do not emit them, and code that checks CPUID sees no ADX. The conversions are covered by the converter tests. ANDN lowering is also executed by an x86-64 test build under Rosetta on the M1; the other conversions were originally checked on an M5 Max, outside this repository's CI. - `--to-intel` replaces the approximate legacy SSE and VEX.128 register forms of RSQRTPS/RCPPS and VRSQRTPS/VRCPPS with [correctly rounded](../../../core/relinker/codegen/src/x86/ReciprocalLowering.cpp) 1/sqrt(x) and 1/x, not with the AMD approximation: its tables are not public. Intel's approximation of 1/sqrt(1.0) is 0x3F7FF000, and two Newton-Raphson steps on it settle on 0.99999994 instead of 1.0; a renormalised identity quaternion then turns into a NaN axis in PPSA21564, which the console does not show. Refined results now reach the exact value; unrefined ones can still differ from the console in the low bits. Each site costs an out-of-line stub (two jumps, a spill below the red zone, SQRTPS/DIVPS). Legacy SSE forms preserve the destination's upper YMM bits; VEX forms clear them. The 256-bit, scalar and memory forms keep the native instruction, as does a site shorter than 5 bytes with no movable instruction after it for the jump (5 sites in PPSA21564, counted before a following RIP-relative instruction could move into the stub; not recounted). -- `--trace-imports` ([entry stub](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp)) exists for Windows output only. Only imports bound through a PLT slot, or through a GOT slot whose symbol is a function, are traced; a function reached through any other relocation, or looked up at run time with `sceKernelDlsym`, is not, and neither are weak imports or the imports of modules loaded at run time +- `--trace-imports` ([Windows entry stub](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp), [Linux entry stub](../../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp)) exists for Windows and Linux output, not macOS. Only imports bound through a PLT slot, or through a GOT slot whose symbol is a function, are traced; a function reached through any other relocation, or looked up at run time with `sceKernelDlsym`, is not, and neither are weak imports or the imports of modules loaded at run time. On Linux the entry stub makes the page of every traced slot readable, writable and executable with `mprotect` before it stores the thunk, so a slot inside `PT_GNU_RELRO` can be replaced; those pages stay writable, and data pages become executable, while tracing is on - [ANDN lowering](../../../core/relinker/codegen/src/x86/AndnLowering.cpp) uses scalar NOT and AND for 32- and 64-bit register and memory operands under `--to-rosetta`, following the ANDN entry in [Intel SDM volume 2A](https://cdrdv2-public.intel.com/812383/253666-sdm-vol-2a.pdf). SF and ZF follow the result, CF and OF clear, and AF and PF are undefined. The scratch register is saved below the 128-byte red zone. A 64-bit stack-relative displacement that exceeds signed disp32 after reserving the spill frame throws; other BMI instructions are not lowered. The M1 test covers register overlap, RSP operands, memory addressing, RIP relocation, flags and red-zone preservation; precise fault delivery inside a spill sequence has not been verified. diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 9e43d509d9..d76932d773 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -9,6 +9,8 @@ ### Unknown function info - [VCC-backed MIMG samplers](../../../core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp): the s104/s105/vcc_lo/vcc_hi tuple follows LLVM's [SGPR register classes](https://github.com/llvm/llvm-project/blob/d8219b47422f7322db252ad29ca8ea87993fb433/llvm/lib/Target/AMDGPU/SIRegisterInfo.td#L452) and [gfx10 sampler operand](https://github.com/llvm/llvm-project/blob/d8219b47422f7322db252ad29ca8ea87993fb433/llvm/lib/Target/AMDGPU/MIMGInstructions.td#L1379), using the existing VCC word reads. Recompiled wave32/wave64 reads are checked by [bilinear and white-border cases](../../../core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleVccSampler.cpp); raw RDNA execution of this tuple on gfx1013 or gfx1036 is unmeasured. +- [MSAA-address image loads on 2D textures](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) - 32-bit `image_load` with DIM 2D_MSAA reads a non-multisampled 2D T# as the same DIM 2D load, ignoring the sample index. Measured on RX 6650 XT (gfx1032), wave32 and wave64, for R32_UINT, R32_FLOAT, RGBA8_UNORM and RGBA8_SRGB, sample indices 0/1/2/7/31/0xffffffff and in-range/out-of-range coordinates; IEEE 0, DX10 clamp 1, denorm32 0, denorm16 3, round32/16 0, fp16 overflow 0. Pinned in ImageMsaaAddressOnPlane.cpp. Not measured on gfx1013. Stores, atomics, mip-bearing or A16 addresses, packed accesses, converted formats, depth-bit views, array-MSAA addresses and image-table accesses are not enabled by this compatibility mode. + - [MUBUF/MTBUF descriptors ending in VCC](../../../core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp): the four dword descriptor at s104 is s104, s105, vcc_lo and vcc_hi, as LLVM's [VCCTuple/SReg_128](https://github.com/llvm/llvm-project/blob/d8219b47422f7322db252ad29ca8ea87993fb433/llvm/lib/Target/AMDGPU/SIRegisterInfo.td#L422) and [buffer resource operands](https://github.com/llvm/llvm-project/blob/d8219b47422f7322db252ad29ca8ea87993fb433/llvm/lib/Target/AMDGPU/BUFInstructions.td) describe. Existing translation maps scalar codes106/107 to the raw VCC words. Recompiled loads/stores, both words, bounds, formats and wave32/wave64 execute on NVIDIA and AMD Vulkan; raw gfx10/gfx1013 hardware is not measured. Higher descriptor bases remain refused; the normal SGPR bound is unchanged. - [Packed pixel ancillary word](../../../core/shader/recompiler/Optimization/src/ConstantFolder.cpp) (shader recompiler) - the sample index at bits 8-11 and the render target index at bits 16-28 follow #295. The sample index is the SampleId builtin in a pixel shader the guest iterates per sample (`PS_ITER_SAMPLE` on a multisampled draw), an equality inferred, not measured. In any other pixel shader it is 0, the iterated sample number of a pixel the hardware does not iterate (also inferred), so reading it does not make the host shade per sample. The render target index is the Layer builtin when the draw exports it (a layered draw, `PA_CL_VS_OUT_CNTL` bit 18; the pipeline specialization constant `LayerExported`) and 0 otherwise, the first layer Vulkan renders when no pre-rasterization stage writes Layer. Reading the unwritten Layer builtin instead makes AMD's Windows driver drop the fragment's writes (#4800); with the constant at 0 the load is folded away. Mesa reads both fields from those bits, with the layer 14 bits wide only from GFX12 on ([ac_nir_lower_intrinsics_to_args.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/bffd91947efc1932da99aca69fc540d6d4201b0c/src/amd/common/nir/ac_nir_lower_intrinsics_to_args.c#L342-L368)). A value passed through a select, move or phi whose every read is an extract inside one of those fields is rebuilt from the two fields alone. Any other read the masked-select pass cannot drop gets the whole word: the primitive class at bits 0-1 (0 point, 1 line, 2 triangle, 3 rectangle), which the draw sets as a pipeline specialization constant ([RasterPrimitiveOf](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp)) in the shaders that read it, plus the sample field, with every other bit zero. That layout comes from the RDNA 3 and 3.5 ISA guides (prim type 1:0, VRS rates 3:2 and 5:4, eye id 7, sample 11:8, render target index 28:16). The GCN register guides give the same prim type and sample fields and put the render target index at 26:16, so the two sources agree for layers below 2048. AMD's LLPC reads the primitive class from bits 1:0 with the same encoding ([LowerGlCompatibility.cpp](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/llpc/lowering/LowerGlCompatibility.cpp#L830-L836)) and without a GFX-level check ([getPrimType](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/lgc/lowering/LowerInOut.cpp#L5307-L5315)). Nobody has checked it on gfx1013. In the whole word only bits 0-1 and 8-11 have a source. Bits 2-7, 12-15 and 29-31 are assumed zero, not measured: the VRS rates (2-5) and the eye id (7) because the driver supports neither variable-rate shading nor multiview, so a title that enables either would read other bits on the console, and bits 6, 12-15 and 29-31 because no source gives them a meaning. The sample field reads SampleId only in iterated shaders. The whole word has the same 0 at bits 16-28. A change that makes draws export a layer must restore the Layer read in both places - [Front-face input with `FRONT_FACE_ALL_BITS`](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (shader recompiler) - with `SPI_BARYC_CNTL.FRONT_FACE_ALL_BITS` set, a back face reads 0 and a front face 0xffffffff. Mesa 22.0 RADV sets the bit ([radv_pipeline.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-22.0.0/src/amd/vulkan/radv_pipeline.c#L5953)) and ACO tests the input with `v_cmp_lg_u32 0` ([aco_instruction_selection.cpp](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-22.0.0/src/amd/compiler/aco_instruction_selection.cpp#L8123-L8128)), so only back = 0 and front ≠ 0 are sourced; PPSA05684's two-sided cloth shaders test it with `v_cmp_lt_u32 0`. The exact front value is not measured. Without the bit the input is ±1.0, as current Mesa reads it (`fgt 0`, `fsign`; [ac_nir_lower_intrinsics_to_args.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/bffd91947efc1932da99aca69fc540d6d4201b0c/src/amd/common/nir/ac_nir_lower_intrinsics_to_args.c)) @@ -23,6 +25,8 @@ ### Functional +- [Windows cache housekeeping](../../../core/shader/recompiler/ShaderDiskCache.cpp): eviction of old versions and temporary files is skipped for `\\?\` and `\\.\` cache roots because the supported WinLibs directory iterator can enumerate the process working directory for long prefixed paths. The active version's `last-used` stamp update still runs; it fails for long `\\.\` aliases with the supported toolchain. Ordinary Windows directory enumeration remains subject to MAX_PATH. + - [f64 compares](../../../core/shader/recompiler/Translation/src/CompareInstructions.cpp) flush denormal inputs to signed zero when bit 0 of the `FLOAT_MODE` f16/f64 denormal field is clear (modes 0 and 2); modes 1 and 3 and an unknown mode keep them. This follows the RDNA2 ISA section 6.4 and LLVM's sign-preserving AMDGPU denormal modes. The gfx1036 audit in [#981](https://github.com/boykopovar/AnyPS5/issues/981#issuecomment-6070046381) reports mode-sensitive `VopcCompareF64` results. Class compares still inspect the original bits. The full four-mode matrix has not been newly measured on AMD hardware or the PS5's gfx1013. - [Unlinked calls](../../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) - the `s_swappc_b64` a table call's compare chain falls back to, when its target matches no linked copy, records the `UnlinkedCall` fault with the target and the program counter and stops the invocation, or continues after the call in programs whose faults do not stop invocations (see `bdaStopsInvocations`). The driver lists those calls in the request (`linkedCallFallbacks`), and it reports the fault like a BDA fault, naming the target: a dispatch that checks its faults when it completes throws, one whose check runs in a deferred write-back logs it and continues. The hardware would jump to the target, which is not modelled. Any other `s_swappc_b64` whose target is not resolved still throws when the shader is compiled. @@ -37,6 +41,7 @@ - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) snapshot holds at most 65536 keys per table, 65536 distinct images and 256 S# entries per shader; a key past a longer table's snapshot, or an entry over the budget, faults when selected. A selected column (a T# the SRT walker evaluates) holds its one record wherever it lies. - [Image tables](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) read by `s_load` through a 64-bit pointer hold the keys the key's known bits allow, at most 65536; a key past them, or an entry at a wrapping, unaligned or unmapped address, faults when selected. - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) whose key is neither a V#-relative scalar read nor a value the SRT walker evaluates (for example a shifted or masked runtime-offset V# read, a per-lane value made uniform by `v_readfirstlane`, or a loop-carried phi) binds every record of the snapshot domain instead of the selected ones. So does a table read at `(key << n) & mask` with such a key: a key domain would select records by the unmasked key, so that key is not narrowed. A key the walker evaluates selects its one record instead, unless the shader may write the memory the key is read from at run time (the same buffer, or any memory when the read or a write goes through a pointer or a GPU-only descriptor); then the column binds the whole domain too. A write to the key's memory through another V# over the same bytes is not detected. A shader whose selected records would pass 64 images or 64 sampled image/sampler pairs binds, for the heaps with the most such reads, the union of their keys in one table instead. +- [Image table pointer indexing](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) propagates 32-bit value bounds through `ReadFirstLane` and `SelectU32` so waterfall key masks keep their bit domain. The SRT walker validates runtime select conditions whose active lane mask implies the condition through `LogicalAnd` conjunctions, assuming unselected inactive lanes need not be uniform across the full wave. - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) T# of an MSAA or FMASK image, or of a color format under a depth compare, faults when selected: tables have no multisampled descriptors and no emulated color compare. - Direct [FMASK image loads](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) read uncompressed `Fmask32_S8_F8` through a single-mip 2D or 2D-array `R32_UINT` snapshot with `Z_64KB_X` addressing. Destination selectors may name X, zero or one. Other FMASK formats, CMASK compression, packed accesses and other layouts throw. The layout follows PAL's [FMASK-to-surface conversion](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/core/addrlib2.cpp#L877) and [32-bit view selectors](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9Device.cpp#L4973). Native RDNA/PS5 image-load results and title rendering are unmeasured; compressed color sampling remains unsupported. - `image_store`, image atomics, PCK2/PCK4 and MIMG BY2/BY4 through an [image table](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) T# fault (`unsupported operation`) when an active lane executes them; the store is dropped and the load reads zeros. A T# whose words the SRT walker evaluates is bound directly instead, for every operation of the shader on it (`DirectImageHandles`), so only T#s keyed per lane or by a value the walker cannot evaluate reach a table with these operations; such a direct T# is read when the work is recorded, so a key its own dispatch writes is read before that write. @@ -46,7 +51,7 @@ - An [image table](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) key with no key domain whose `key * stride` wraps past 2^32 to an offset between records faults only when the build already expects a fault; otherwise the fault buffer is not read and it reads zeros. - [MaskedSelectEliminator](../../../core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp) can discard inactive source lanes through `ReadFirstLane` only when its read mask implies the write mask and its result is unobserved outside the read mask. A loop-local read mask requires result uses in the same block without phi transport. The empty-EXEC lane-zero fallback follows AMD's [Vega ISA, section 12.8, V_READFIRSTLANE_B32, page 127](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/vega-shader-instruction-set-architecture.pdf) and the existing SPIR-V emitter. The optimization is covered by synthetic IR tests but has not been compared with console hardware; a proof that needs a read mask of a kind that is already being solved is refused. - [DenormalFlushEliminator](../../../core/shader/recompiler/Optimization/src/DenormalFlushEliminator.cpp) drops the result flush of a `v_mul_f32` or `v_mul_legacy_f32` product behind its nearest-even tininess check (see f32 denormals) when both factors are denormal-free: a product the check keeps is then zero, infinite, NaN or at least 2^-126 in magnitude, provided the driver rounds it as it rounds the check's multiply. -- [Shader modules built at registration](../../../core/shader/recompiler/Recompiler.cpp) (shader recompiler): on devices with graphics pipeline libraries a vertex or pixel module reads buffer V# words, image modes, swizzles and mip ranges, depth-compare state, export mappings and vertex component selectors at run time instead of specializing on them per draw, so its libraries can be built when the shader is registered. A module that would exceed `SpirvTarget::registrationWordLimit` (200000 SPIR-V words) is emitted specialized as before: on RADV (Radeon 660M, Mesa 26.2.4) a sample of 60 Astro's Playroom pixel shaders compiled 1.9 to 2.2 times slower in registration form below 141k words and 3.8 to 5.7 times slower above 280k words (none in between), so the limit sits in that gap; it was not measured on other drivers. Texel-offset folding, the format word of a buffer accessed with the descriptor's format (`buffer_load_format_*`; as runtime values every format's decoder is compiled, which made CI's lavapipe JIT ten times slower) and the vertex attribute numeric class stay specialized, so shaders with the first two, and pixel shaders whose image needs a multisampled descriptor, get no registration result; these, and every draw whose fixed-function state differs from the prebuild's, still build their libraries at the draw. Compute and the other stages stay specialized: in registration form the `agc_driver_dword_pattern_fill` compute shader hung CI's lavapipe (Mesa 25.2.8) when built with `ANYPS5_ENABLE_SPIRV_TOOLS=ON`, not on RADV, and the cause was not found. +- [Shader modules built at registration](../../../core/shader/recompiler/Recompiler.cpp) (shader recompiler): on devices with graphics pipeline libraries a vertex or pixel module reads buffer V# words, image modes, swizzles and mip ranges, depth-compare state, export mappings and vertex component selectors at run time instead of specializing on them per draw, so its libraries can be built when the shader is registered. A module that would exceed `SpirvTarget::registrationWordLimit` (200000 SPIR-V words) is emitted specialized as before: on RADV (Radeon 660M, Mesa 26.2.4) a sample of 60 pixel shaders of a retail title compiled 1.9 to 2.2 times slower in registration form below 141k words and 3.8 to 5.7 times slower above 280k words (none in between), so the limit sits in that gap; it was not measured on other drivers. Texel-offset folding, the format word of a buffer accessed with the descriptor's format (`buffer_load_format_*`; as runtime values every format's decoder is compiled, which made CI's lavapipe JIT ten times slower) and the vertex attribute numeric class stay specialized, so shaders with the first two, and pixel shaders whose image needs a multisampled descriptor, get no registration result; these, and every draw whose fixed-function state differs from the prebuild's, still build their libraries at the draw. Compute and the other stages stay specialized: in registration form the `agc_driver_dword_pattern_fill` compute shader hung CI's lavapipe (Mesa 25.2.8) when built with `ANYPS5_ENABLE_SPIRV_TOOLS=ON`, not on RADV, and the cause was not found. - [SPIRV-Tools optimization](../../../core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp) skips CCP and Simplification for modules with storage-only 8- or 16-bit types because the pinned folder produces illegal narrow constants. This also disables unrelated constant folding; the restricted full pipeline mirrors the pinned performance recipe. - [SRT signed comparison](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp) evaluates `SLessThan32` as signed 32-bit less-than, matching the existing SPIR-V backend and [SPIR-V `OpSLessThan`](https://registry.khronos.org/SPIR-V/specs/unified1/SPIRV.html#OpSLessThan). Signed boundary descriptor expressions are tested on the host; native RDNA2/PS5 execution and title behavior remain unverified. - [MIMG `tfe` and `lwe`](../../../core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp) never write the status VGPR after the data. On hardware it is written only when a texel fetch hits an unmapped page of a partially resident texture, or, for `lwe` on sample/gather, when the LOD is below the T# `MIN_LOD_WARN`; texture memory is always resident here, the LOD warning (texture-streaming feedback, like the T# `MIN_LOD_WARN` itself) is never reported, and `lwe` on `image_get_lod` throws. @@ -55,11 +60,12 @@ - [2D stores to 1D storage images](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp): an `image_store` with DIM 2D through a 1D T# writes texel x and ignores y, as measured on RDNA2 (gfx1035) for y from 0 to 4 and 0xffffffff, single- and four-component formats and a 1x1 8_8_8_8_UNORM T#: the T# type decides the addressing, DIM only how many address VGPRs are read. Storage loads, atomics and `image_store_mip` through such a pair still throw - [MIMG PCK2/PCK4 loads](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_load[_mip]_pck2/pck4`, opcodes 0x70, 0x71, 0x73 and 0x74) were measured on RDNA2 (gfx1035) with the [hardware oracle](../HW_ORACLE.md) for 2D images with identity swizzle. With data mask 0x1, they write one dword: the raw bits of the N texels (PCK2: 2, PCK4: 4) from `x & ~(N - 1)` on row y, concatenated (element k at bit `k * texel bits`) and zero-extended. The dword is zero when the mip level is past the last level, `x + N - 1` is outside the level's width (unaligned x, unsigned), or y is outside its height. Data only comes back when N texels fit in 32 bits: R8, R16 or RG8 for PCK2, R8 for PCK4. Wider texels (R32, RG16, RGBA8) and masks other than 0x1 write zeros on hardware. Those masks throw at decode, and formats outside UNORM/UINT/SINT R8, R16 and RG8 throw. #1314's earlier measurement saw only zeros because it used masks 0x3 and 0xf. The stores (0x76, 0x77, 0x79 and 0x7a) decode but throw at translation. They are not measured, and opcode 0x76 on 32_32_32_32_FLOAT hung the GPU (#1314). - [3D image accesses bound to a 2D or 2D-array T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) follow CouchRiv's [hardware oracle](../HW_ORACLE.md) measurement on RDNA2 (gfx1036, #2023) and adriadam10's (gfx1035, #2140), not the PS5's gfx1013. On a 2D T#, `image_sample_lz` ignores r and reads the (u,v) texel, `image_load` and `image_load_mip` read the (x,y) texel when z is 0 and return zero otherwise, and `image_get_resinfo` returns (width, height, 1, levels). On a 2D-array T#, these accesses behave like dim 2D-array: r or z is the layer, and `image_get_resinfo` returns the layer count as its third component. Every other access of such an image, including samples with an LOD, bias, derivatives, offset or comparison, gathers, `image_get_lod` and stores, keeps throwing because it is not measured. +- [2D-array samples through a 3D T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) (`image_sample`, `image_sample_l` and `image_sample_lz` with DIM 2D_ARRAY) sample the volume as DIM 3D does: the third address VGPR is the normalized r coordinate, addressed by the sampler's Z mode (only wrap was measured), not a layer index, and a plain sample in a pixel shader takes its LOD from the derivatives of all three coordinates. Measured on RDNA2 (RX 6700 XT, gfx1031, driver 32.0.21045.5002, Windows 11) with the [hardware oracle](../HW_ORACLE.md) template run through the driver's comgr and HIP runtime, not on the PS5's gfx1013: on 16x16x4 R32_FLOAT 3D T#s with one and three levels, the three instructions with DIM 2D_ARRAY returned the same value as with DIM 3D in all 256 lanes of 66 cases (point, bilinear and z-linear filters, wrap addressing, r from -0.125 to 3, quad steps of 1 to 4 texels in u and v and of 0 to 4 slices in r, LOD 1). Other samples of such a pair (bias, LOD clamp, derivatives, offsets, comparison, 16-bit addresses), gathers, `image_get_lod` and a sample mixed with a load of the same image keep throwing because they are not measured. The host sampler ignores `Z_FILTER`, as for every 3D sample, so r is filtered with the XY filter. - [2D accesses through a cube T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) (#4139) read the T#'s `BASE_ARRAY` face as a cube access with face coordinate 0 does, measured on RDNA1 (gfx1010, RX 5700 XT) with the [hardware oracle](../HW_ORACLE.md) template run through the Windows driver's comgr and HIP runtime on 8x8 and 32x32 32_FLOAT cubes (`DEPTH` 5 and 11, `BASE_ARRAY` 0 and 6): `image_sample_lz`, plain `image_sample` (one level, and six levels in 2x2 compute quads) and `image_sample_l` (the third address VGPR is the LOD, integer and fractional) with DIM 2D equal DIM CUBE with face 0 on every row, with point and bilinear filters, wrap, clamp and mirror addressing and `DISABLE_CUBE_WRAP` 0 and 1, and the VGPR after the two coordinates is not read; `image_load` with DIM 2D reads texel (x, y) of the `BASE_ARRAY` face. So the face coordinates are cube coordinates in [1, 2]: the texel is `floor((u - 1) * width)`. They are translated through the cube mode DIM CUBE accesses use, which samples the face of the 2D-array view at (u - 1, v - 1) with the S# addressing. That matches the hardware inside the face, except within half a texel of its edges with a bilinear filter and `DISABLE_CUBE_WRAP` 0, where the hardware blends texels of the neighbouring faces (also measured on RDNA2, gfx1035, 8x8 cubes with `BASE_ARRAY` 0 and 6). Outside it the hardware clamps a coordinate below 1 to the first texel under every filter, wrap mode and `DISABLE_CUBE_WRAP` setting measured, clamps a coordinate of 2 or more to the last texel when `DISABLE_CUBE_WRAP` is 0 (point filter) and applies the S# addressing to it when it is 1; the translation does none of this, as for DIM CUBE. Only `image_sample_lz`, plain `image_sample`, `image_sample_l`, mip-less `image_load`, and mip-less `image_store` to an image the program only stores to take this mode. Such a store writes texel (x, y) of the `BASE_ARRAY` face, measured on RDNA2 (gfx1036, [by simonbbby](https://github.com/boykopovar/AnyPS5/pull/6799#issuecomment-6217609589), not the PS5's gfx1013) with the [hardware oracle](../HW_ORACLE.md) on 8x8 mip-less cube arrays (`DEPTH` 11) of 32_UINT, 32_FLOAT and 16_16_16_16_FLOAT ([the last also by simonbbby](https://github.com/boykopovar/AnyPS5/pull/6799#issuecomment-6224023050), with exactly representable half values) with `BASE_ARRAY` 0 and 6, wave32 and wave64: the 64 stored texels land on that face and every other byte of the 64 KiB backing is unchanged. Other formats, and Crimson Desert's own 256x256 tiled descriptor, are not measured. Other DIM 2D accesses through a cube T# (gathers, bias, comparison, offsets, derivatives, atomics, stores to an image the program also reads, stores of formats written through a conversion such as 32_SINT, mip loads and stores) still throw. - [Unnormalized S#s](../../../core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp) are accepted only when direct and used only by 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. @@ -69,6 +75,7 @@ - [Samples and gathers](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) of 8_SRGB and 8_8_SRGB textures read an RGBA8 sRGB copy of the texture (libSceAgcDriver, [Texture.cpp](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp)) on a device that cannot sample R8_SRGB or R8G8_SRGB but can blit R8 or RG8 to RGBA8, so they use that device's own decode. They still throw on a device that cannot blit, and with `APS5_SRGB_SHADER_DECODE=1`. `APS5_SRGB_SHADER_DECODE=copy` also takes the copy on a device that samples these formats natively, so the copy can be tested on any device that can blit R8 or RG8 to RGBA8; on a device that cannot blit, it behaves like `APS5_SRGB_SHADER_DECODE=1`. A multisampled or depth view of these formats gets no copy. - [Bindless image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) entries of 8_SRGB and 8_8_SRGB textures read zeros, like converted formats, on a device that cannot sample R8_SRGB or R8G8_SRGB. - [Shader MODE register access](../../../core/shader/recompiler/Translation/src/RoundingMode.cpp) starts from the rounding fields of `GuestContext::floatMode` and follows immediate rounding writes (`s_round_mode`, `s_setreg_imm32_b32` on MODE bits 0-3) through the control flow graph; `s_getreg_b32` of those bits returns the tracked value. Only `v_cvt_f32_i32`, `v_cvt_f32_u32`, `v_ldexp_f32`, the f16 conversions and `v_ldexp_f16` (with f16 denormals kept) and the instructions that already read `FLOAT_MODE` follow a rounding mode set at run time; any other float instruction that runs while a directed rounding mode differs from the initial one throws at that instruction. Rounding bits written from an SGPR (`s_setreg_b32`) or that differ between the paths reaching an instruction throw where they are read or used. A shader without `FLOAT_MODE` metadata throws on any rounding change. Non-rounding register reads still throw; `s_setreg_b32` writes to other hardware registers are still ignored. Tests follow the public RDNA2/LLVM contract, not console measurements. +- [Flat scalar-read reachability](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) keeps both successors active for non-clean predicates and loop headers, so an inaccessible direct root on such a path still refuses capture. Skipping a read under a clean, evaluable scalar branch follows RDNA2 conditional PC semantics and synthetic fixtures; console execution and title behavior are unverified. - A [flat SRT read](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) through a null or unmapped pointer loaded from memory raises a [BDA fault](../../../core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp) only if the program executes it, then stops or reads zero like a BDA read. - Repeated `s_load`s of one [flat SRT](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) dword through a null or unmapped pointer report one of their pcs, and an `s_load` whose inaccessible dwords are all unused, or only address repeated loads, reports no fault of its own. - A value read through a null or unmapped pointer still throws at record time if a [uniform fill or table base](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) needs it, or if the pointer comes from user data and is not null, even if the console skips the read. @@ -77,7 +84,7 @@ - [2D accesses through a 1D T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) read the texel the 1D addressing selects and ignore the second coordinate, as simonbbby measured on RDNA2 (gfx1036) in #2237 for `image_sample_lz` and mip-less `image_load` with point filtering and wrap, clamp and border addressing; `image_sample_l` was measured the same way on gfx1036 against `image_sample_l` with DIM 1D through the same T#, for u inside, on and past the edges, v of 0, 0.5, 1, -0.25 and 3, LOD -1 to 6 on a 5-level R32_FLOAT texture, point, linear and linear-mip filtering, wrap, clamp and border addressing and HEIGHT or DEPTH 3: all 4725 rows matched. The PS5's gfx1013 is not measured. A plain `image_sample` (implicit LOD) through such a pair takes its LOD from the derivatives of the first coordinate alone, measured on RDNA2 (gfx1036) with the [hardware oracle](../HW_ORACLE.md) on the quads of a compute kernel (wave32 and wave64, IEEE off, DX10 clamp on, denormals preserved, round to nearest even): on a 16-texel, 5-mip 32_UINT T# with point filters it picks the same mip and texel as DIM 1D for first-coordinate steps of 0, 1, 2, 4 and 8 texels per lane along x or along y, whatever the second coordinate does (normalised steps up to 16 per lane), while the same rows on a 16x16 2D T# move the mip with the second coordinate. Outside the pixel stage a plain `image_sample` is emitted with LOD 0 for every image type, so this pair reads mip 0 there, where the kernel measurement shows the quad's derivatives being used. `image_sample_c_lz` and plain `image_sample_c` through such a pair compare against the texel the DIM 1D comparison with the same reference and first coordinate reads, measured on RDNA1 (gfx1010, RX 5700 XT) with the [hardware oracle](../HW_ORACLE.md) template run through the Windows driver's comgr and HIP runtime (wave32 and wave64, IEEE off, DX10 clamp on, denormals preserved, round to nearest even): 8-texel 32_FLOAT and 16_UNORM T#s, also with HEIGHT 3, point and bilinear filters, wrap, clamp and opaque-white border addressing, less and gequal functions, second coordinates 0, 0.5, 1, 1.5, -0.25 and 3; on a 64-texel, 7-mip 32_FLOAT T# `image_sample_c` picks its LOD from the first coordinate's steps alone (1, 4 and 16 texels per lane give mips 0, 2 and 4; second-coordinate steps up to 64 texels leave it unchanged). Only comparisons the runtime does natively (32_FLOAT, 16_UNORM) reach the 1D view; comparisons the runtime emulates for other color formats still throw through such a pair. 2D-array `image_sample_l` and `image_sample_lz` through a 1D T# read as DIM 1D too, ignoring v and the slice: measured on gfx1036 with the hardware oracle (template assembled by the AMD Windows driver's `amd_comgr_2.dll` and launched through its `amdhip64_6.dll`) against the same sample with DIM 1D on a 16-texel 8_8_8_8 UNORM T# with 1 or 5 levels, HEIGHT or DEPTH 3, for u inside, on and past the edges, v of 0, 0.5, 1, -0.25 and 3, slices 0, 1, 2.7, -1 and 5, LOD -1 to 6, and the five filter and address cases above: all 48000 rows matched. A 2D-array `image_load` through a 1D T# is not the same: on gfx1036 it returns the DIM 1D texel only at slice 0, and zeros at slices 1, 2, 5 and -1 even with DEPTH 3, so it, the plain `image_sample` and the other 2D-array accesses still throw. Other 2D accesses through a 1D T# (storage, depth, packed, BY loads, mips other than explicit LOD, explicit LOD with comparison, bias, offsets, derivatives) still throw. - [3D image accesses bound to a 1D T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) follow a [hardware oracle](../HW_ORACLE.md) measurement on RDNA1 (RX 5700 XT, gfx1010; wave32 and wave64, IEEE off, DX10 clamp on, denormals preserved, round to nearest even), not the PS5's gfx1013, on 8x1 R8G8B8A8_UNORM linear 1D T#s with HEIGHT and DEPTH 0 or 3 and point filters. `image_sample_lz` reads the texel u selects for v and r in {0, 0.5, 1, 1.5, -0.25, 3}, with wrap, clamp and border addressing, and v and r never produce the border; u wraps, clamps and borders as with DIM 1D. Mip-less `image_load` reads the x texel for any y in {0, 1, 2, 3, -1} when z is 0, returns zero for z in {1, 2, 3, -1}, whatever DEPTH says, and returns zero for x past the width. A plain `image_sample` (implicit LOD) on the quads of a compute kernel takes its texel and LOD from u alone: on a 64-texel, 7-mip 1D T# steps of 1, 4 and 16 texels per lane pick mips 0, 2 and 4, as DIM 1D `image_sample_l` does, and steps in v or r alone stay at mip 0. Outside the pixel stage a plain `image_sample` is emitted with LOD 0, as for 2D accesses through a 1D T#. Every other 3D access of such an image (`image_load` with mips, storage, explicit LOD, bias, derivatives, offsets, comparison, gathers, queries) keeps throwing. SONIC SUPERSTARS (PPSA06888) samples a 1x1 1D T# this way with plain `image_sample` and `image_load`. - [Emulated comparison of a 2D-array access through a 2D T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) samples the 2D image and ignores the slice, as the native modes of a 2D-array access through a 2D T# already do. This follows the T# type deciding the addressing; it is not measured on hardware for comparison samples -- [Flat aperture bases](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (core/shader/recompiler) - the layout follows the gfx9+ one in the AMD KFD driver's `kfd_flat_memory.c` (`MAKE_LDS_APP_BASE_V9`, `MAKE_LDS_APP_LIMIT`): each aperture is a 4 GB window whose base is the high dword, so a flat address is routed by its whole high dword, as measured on RDNA2 (gfx1035: `src_shared_base` 0x00010000_00000000, `src_shared_limit` 0x00010000_ffffffff). The PS5's base values, high dwords 0x80000000 (shared, `SharedApertureHigh`) and 0x70000000 (private, `PrivateApertureHigh`), are inferred from GTA V's traversal addresses, not measured on a console or read from SH_MEM_BASES. The scalar aperture sources read the same two values +- [Flat aperture bases](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (core/shader/recompiler) - the layout follows the gfx9+ one in the AMD KFD driver's `kfd_flat_memory.c` (`MAKE_LDS_APP_BASE_V9`, `MAKE_LDS_APP_LIMIT`): each aperture is a 4 GB window whose base is the high dword, so a flat address is routed by its whole high dword, as measured on RDNA2 (gfx1035: `src_shared_base` 0x00010000_00000000, `src_shared_limit` 0x00010000_ffffffff). The PS5's base values, high dwords 0x80000000 (shared, `SharedApertureHigh`) and 0x70000000 (private, `PrivateApertureHigh`), are inferred from PPSA04263's traversal addresses, not measured on a console or read from SH_MEM_BASES. The scalar aperture sources read the same two values - [Flat accesses outside the aperture routing](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (core/shader/recompiler) - only compute stages route flat accesses, so graphics-stage flat accesses to the shared or private aperture still go to BDA. In compute, a 64-bit flat atomic whose address is in the shared aperture takes the workgroup lock (`ldsLock`) for its two-dword update, and every such atomic requests that lock, so a shader with one pays for it even on global addresses (the path is exercised by `agc_driver_global_atomics_float`). Inferred, not measured on LDS or scratch: an aperture access that is not inside one dword is done byte by byte, an unaligned aperture atomic records the BDA unaligned fault, and LDS or scratch accesses past their end are dropped or read zero. - On a host subgroup narrower than 32 lanes, [`threadBit`](../../../core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp) reads an EXEC written as a value (`s_mov_b32`/`s_mov_b64`) at bit (subgroup invocation id) of EXEC_LO, so lanes at or above the subgroup size read a lower lane's bit. - [EXEC read as a value](../../../core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp) (`s_mov_b64 sN, exec`, the `s_*_saveexec` copy, ALU sources) is limited to host lanes. Pixel reads retain helper bits in quads with an active fragment, preserving whole-quad masks across copies. Quad expansion follows the [RDNA 2 ISA, section 12.3, S_WQM_B32/B64, page 111](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf#page=119); save/restore followed by implicit-LOD sampling is checked on an RX 6700 XT, not on a console. The console also holds lanes the host lacks, e.g. 32-63 of a wave64 on 32 host lanes. @@ -89,16 +96,19 @@ - [Mesh layer output](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - a mesh shader writes each primitive's layer through a one-member `Block` struct array whose member is decorated `BuiltIn Layer` and `PerPrimitiveEXT`, as glslang declares `gl_Layer` inside `gl_MeshPerPrimitiveEXT`, not through a standalone `PerPrimitiveEXT` `Layer` array. Both forms pass `spirv-val`, but AMD's Windows driver (32.0.21045.5002, RX 6700 XT) crashes in `amdvlk64.dll` creating a pipeline whose mesh shader writes the standalone form, whether the layer is signed or unsigned and whatever the target, while the block form draws, also with a pixel shader that reads the layer. - [Shader recompilation](../../../core/shader/recompiler/Recompiler.cpp) currently occurs right before it was transferred to Vulkan with caching, but should be moved to the [relinker](../../../core/relinker/main.cpp) stage. For this purpose, [shader/recompiler](../../../core/shader/recompiler) was written completely independently from [libs/prx](../../../core/libs/prx). - [v_interp_p1ll_f16, v_interp_p1lv_f16 and v_interp_p2_f16](../../../core/shader/recompiler/Translation/src/AttributeInstructions.cpp) read the 16-bit halves of each vertex's parameter through `VK_KHR_fragment_shader_barycentric`, as fixed-function interpolation cannot split a 32-bit attribute. Without the extension the pixel shader takes the [barycentric emulation](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp), whose generated geometry stage passes each vertex's parameter to it as a flat input, so the halves arrive bit for bit; a device without geometry shaders takes the tessellation stages of the same emulation instead, which no 16-bit interpolation test has run through yet. P10 and P20 are the vertex differences rounded to f16 to nearest even, which is not confirmed on hardware. v_interp_p1ll_f16 and v_interp_p1lv_f16 round the exact `P10 * I + P0` once to f32 and v_interp_p2_f16 rounds the exact `P20 * J + S2` once to f16, as measured on RDNA2 (#697); the exact sums are computed in f64 or, on a device without the Float64 capability, in 64-bit integers: the exact product is added with a sticky bit and rounded once. They follow the shader's float mode, measured on RDNA2 (gfx1035) in all eight combinations of `IEEE_MODE` and kept/flushed f32 and f16 denormals over 768 special and 1,024 random rows. With IEEE=1 a NaN operand is returned quieted; with IEEE=0 it is returned unquieted, except that v_interp_p2_f16's f16 result is always quiet. Flushed f32 denormals flush I/J, v_interp_p2_f16's f32 S2 and the f32 result; flushed f16 denormals flush P0, P10/P20, v_interp_p1lv_f16's f16 S2 and the f16 result. Denormal modes 1 and 2 (flush only inputs or only outputs) and rounding other than to nearest even are not measured and throw. An unknown mode follows IEEE=0 with denormals kept. Lavapipe has no barycentrics, so the draw test is skipped in CI; `agc_driver_interpolation_modes` checks the integer arithmetic on every device, and the f64 arithmetic on devices with Float64. It checks both against the hardware rows in every mode, and against a CPU reference on random rows in every mode and f16 rounding. -- Without `VK_KHR_fragment_shader_barycentric`, a pixel shader whose I/J VGPRs [only feed `v_interp_p1_f32`/`v_interp_p2_f32` of their own pair](../../../core/shader/recompiler/Optimization/src/HostInterpolationChecker.cpp) reads the attribute Vulkan interpolated. Any other read of them or of a raw vertex parameter takes the weights and the three vertex values from a [generated geometry stage](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp), which needs `geometryShader` and a vertex shader before it. On a device with `tessellationShader` but no `geometryShader` (MoltenVK), a triangle list without primitive restart takes them from [generated tessellation stages](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp) instead: each triangle is one three-point patch at tessellation level 1, the evaluation stage writes the corner `gl_TessCoord` points at as the weights, and the vertex clip distances are forwarded: the control stage reads them as the separate `ClipDistance` array the vertex shader writes, so the vertex-to-control interfaces match, and passes them on inside its per-vertex block, which the evaluation stage reads and writes. MoltenVK 1.4.2 (SPIRV-Cross) cannot translate a separate `ClipDistance` array in a tessellation evaluation stage (`no member named 'gl_ClipDistance' in 'main0_out'`), so the two generated stages keep it in their blocks. A flat parameter takes the first vertex's value there, as the geometry stage's strip gives it; the last provoking vertex throws. The patch keeps the triangle's facing with `VertexOrderCw`, checked with face culling on MoltenVK 1.4.2 (Apple M2) only. Tessellation, mesh and rect-list pipelines, primitives other than triangles, triangle strips and fans without `geometryShader`, more than 32 fragment input locations and interfaces beyond the device's geometry or tessellation limits throw. +- Without `VK_KHR_fragment_shader_barycentric`, a pixel shader whose I/J VGPRs [only feed `v_interp_p1_f32`/`v_interp_p2_f32` of their own pair](../../../core/shader/recompiler/Optimization/src/HostInterpolationChecker.cpp) reads the attribute Vulkan interpolated. Any other read of them or of a raw vertex parameter takes the weights and the three vertex values from a [generated geometry stage](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp), which needs `geometryShader` and a vertex shader before it. On a device with `tessellationShader` but no `geometryShader` (MoltenVK), a triangle list without primitive restart takes them from [generated tessellation stages](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp) instead: each triangle is one three-point patch at tessellation level 1, the evaluation stage writes the corner `gl_TessCoord` points at as the weights, and the vertex clip distances are forwarded: the control stage reads them as the separate `ClipDistance` array the vertex shader writes, so the vertex-to-control interfaces match, and passes them on inside its per-vertex block, which the evaluation stage reads and writes. MoltenVK 1.4.2 (SPIRV-Cross) cannot translate a separate `ClipDistance` array in a tessellation evaluation stage (`no member named 'gl_ClipDistance' in 'main0_out'`), so the two generated stages keep it in their blocks. A flat parameter takes the provoking vertex's value there, the first or with `PROVOKING_VTX_LAST` the last, and every corner of the patch writes it, so the host's own provoking vertex does not matter and `VK_EXT_provoking_vertex` is not needed; a flat per-vertex parameter with the last provoking vertex still throws. The patch keeps the triangle's facing with `VertexOrderCw`, checked with face culling on MoltenVK 1.4.2 (Apple M2) only. Tessellation, mesh and rect-list pipelines, primitives other than triangles, triangle strips and fans without `geometryShader`, more than 32 fragment input locations and interfaces beyond the device's geometry or tessellation limits throw. - [v_fma_f64 and v_div_fmas_f64](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp) with a subnormal operand or result round once, as IEEE 754 fusedMultiplyAdd does with f64 denormals kept, measured on RDNA2 (gfx1035) at IEEE=0 and IEEE=1, `v_div_fmas_f64` with VCC clear and set. The f16/f64 denormal field of `FLOAT_MODE` (bits 7:6) is not read: when it flushes, the hardware flushes the operands and the result of `v_fma_f64` (tininess before rounding) and only the result of `v_div_fmas_f64`, while these keep them. Shaders with f64 instructions declare `DenormPreserve` for 64-bit floats only when `shaderDenormPreserveFloat64` is true and Vulkan reports `denormBehaviorIndependence` as `ALL`; other independence modes are not handled by the current backend because they require coupled execution modes. See [VkShaderFloatControlsIndependence](https://docs.vulkan.org/refpages/latest/refpages/source/VkShaderFloatControlsIndependence.html). Elsewhere (including lavapipe), the driver may flush f64 subnormals, and `agc_driver_f64_fma_subnormals` is skipped. +- [v_floor_f64, v_ceil_f64, v_fract_f64, v_frexp_mant_f64 and v_frexp_exp_i32_f64](../../../core/shader/recompiler/Translation/src/Float64Instructions.cpp) follow the `FLOAT_MODE` f16/f64 denormal field: a denormal input is flushed to a zero of its sign when bit 0 is clear (0 and 2), and `v_fract_f64` flushes a denormal result, the fraction of a positive denormal input, to +0 when bit 1 is clear (0 and 1). `v_trunc_f64` and `v_rndne_f64` give the same results either way and are unchanged. Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) at IEEE=0 and IEEE=1 with denorm16 0..3 over 23 f64 values (zeros, denormals, minimum normals, ±0.5, ±1, values next to 1, the largest finite value, infinities and quiet and signaling NaNs) and 16 of them with neg and abs modifiers. Shaders without a known float mode keep the previous lowering. The PS5's gfx1013 is not measured. +- [v_add_f64 and v_mul_f64 denormals](../../../core/shader/recompiler/Translation/src/Float64Instructions.cpp) follow the `FLOAT_MODE` f16/f64 denormal field; the PS5's gfx1013 is not measured. Inputs flush to a zero of their sign when bit 0 is clear (0 and 2). When bit 1 is clear (0 and 1), a sum below 2^-1022, or a product whose exact value rounded to 53 significant bits with an unbounded exponent is below 2^-1022, flushes to a zero of its sign, so `0x2000000000000001 * 0x1ffffffffffffffe` (exactly 2^-1022 - 2^-1126) rounds to 2^-1022 and is kept. Measured on RDNA2 (gfx1031, RX 6700 XT, AMD Windows driver 32.0.21045.5002, HIP) at IEEE=0 and 1, denorm16 0..3, over 576 pairs of 24 f64 values (zeros, denormals, minimum normals, values next to 1 and to 2^-1022, products just below 2^-1022, the largest finite value, infinities), with neg, abs and clamp. `v_fma_f64`, `v_div_fmas_f64` and shaders without a known float mode keep the previous lowering. - [SINT storage images](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) of 8- and 16-bit components saturate each stored 32-bit value to the component range, as adriadam10 measured for image stores on RDNA2 (gfx1035) in #944 (448 rows match); the PS5's gfx1013 is not measured. A D16 store sign-extends each 16-bit half before that saturation, as measured on RDNA2 (gfx1035) for `image_store d16` to all eight SINT formats with DMASK 0xf, 0x7, 0x5, 0x3, 0x2, 0xa and 0x1 (1792 rows match). - [`StoreTexel`](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) writes channel 1 of a two-channel format from the data slot whose DST_SEL is Y, for SINT and UINT alike. With DST_SEL X,Y,Z,W, gfx1035 writes it from the W slot, as adriadam10 measured in #944. - [v_ldexp_f16](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follows the `FLOAT_MODE` f16 denormal field: bit 0 preserves the half input and bit 1 the half result. Otherwise a denormal input is flushed to its signed zero, and a result whose exact scaled value is below the minimum normal (2^-14) is flushed to its signed zero before rounding, so `v_ldexp_f16 0x07ff, -1` gives +0 instead of 0x0400. Measured on RDNA2 (gfx1036) with round to nearest, IEEE 0 and 1 and denorm16 0..3, over every half magnitude 0x0000..0x0fff of both signs scaled by 2^-1 and 2^0 (16384 rows per mode): no mismatch. NaN and infinity handling and destination high-half preservation are unchanged. Shaders without a known float mode keep the previous lowering. - [v_div_fixup_f64](../../../core/shader/recompiler/Translation/src/Float64Instructions.cpp) flushes its src1 and src2 to a zero of the same sign when bit 0 of the `FLOAT_MODE` f16/f64 denormal field is clear (0 and 2) and keeps a denormal src0 and result in every mode, as `v_div_fixup_f32` does with the f32 field. Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) over 1728 triples of 12 f64 values (zeros, denormals, the minimum normal, ±1, the largest finite value, infinity and quiet and signaling NaNs) at IEEE=0 and IEEE=1 with denorm16 0..3. Shaders without a known float mode keep the previous lowering. The PS5's gfx1013 is not measured. - [v_mullit_f32](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follows the hardware measured with the MODE register at IEEE=1 and denormals kept (a signaling NaN src0 is quieted only with IEEE=1, as measured at IEEE=0 too; a positive denormal src2 counts as positive); with f32 denormals flushed it follows the f32 denormals entry below, and denormal modes 1 and 2 throw. - [v_mad_legacy_f32 and v_mac_legacy_f32](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (VOP3 0x140, VOP2 0x06) round the product to f32 before the add, give a +0 product for a zero factor, and flush their f32 denormal sources, their product and their result to a zero of the same sign in every f32 denormal mode, an exception to the `FLOAT_MODE` model in [f32DenormalFlushFor](../../../core/shader/recompiler/Translation/src/VectorInstructions.cpp). Measured on gfx1010 (RX 5700 XT) with `tools/hw-oracle/template.s`, assembled by the AMD Windows driver's `amd_comgr_2.dll` and launched through its `amdhip64_6.dll` instead of `oracle.c` and ROCr, over the 32 rows of the five `FmaLegacy.cpp` and the four `MacLegacy.cpp` encodings: identical in wave32 and wave64 at IEEE 0 and 1 with f32 denormal modes 0 to 3, except that the two signaling NaN rows are quieted only with IEEE=1 (#1281, #1290). A sum of two zeros takes its sign on the bits (+0 unless both are -0), as lavapipe returned -0 for +0 + -0. The PS5's gfx1013 is not measured; LLVM decodes the encoding as `v_mad_legacy_f32` there, as on gfx1010. RDNA2 differs: on gfx1031 (RX 6700 XT, measured by pandather in #1281) the encoding is `v_fma_legacy_f32` and fused, with the same zero rule and denormal-source flush, so an RDNA2 measurement is not evidence for the console. +- [v_mad_f32, v_mac_f32, v_madak_f32 and v_madmk_f32](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (gfx10.1 only) leave f32 denormals and NaN bits to the driver, unlike the console. Measured on gfx1010 (RX 5700 XT, #5782): the product is rounded before the add; f32 sources, product and result are flushed to a zero of the same sign in every f32 denormal mode; the product is flushed when tiny after rounding to 24 bits with an unbounded exponent; NaN results follow the fma rule, an invalid product beating a NaN src2; in round mode 2 a sum of zeros of opposite signs is -0. Translating this cost 15× `main` on a `v_mac_f32` chain on the RX 5700 XT and 23× on an RTX 3070, and a never-taken branch around it 6×, so it waits for the SPIR-V float controls of #981. - [v_fma_mixlo_f16 and v_fma_mixhi_f16](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) round the exact product-sum once to f16 with the arithmetic of v_interp_p2_f16, in f64 or, without the Float64 capability, in 64-bit integers. On RDNA2 (gfx1036) they follow the `FLOAT_MODE` f16 round and denormal fields, not the f32 round field: -1.5 * 2^-24 + 2^-126 from f32 sources (0xbfc00000, 0x33800000, 0x00800000) gives 0x8001 with f16 denormals kept and round to nearest even, 0x8000 with f16 denormals flushed and 0x8002 with f16 round toward -infinity. With f16 denormals kept, every f16 rounding mode is translated: the exact product-sum is rounded once in the mode's direction. All four modes match RDNA2 (gfx1036) on the 2,233 rows of the #2631 sweep, including `FLOAT_MODE` 0xcc (toward zero, used by ps5-opengl) and the exact cancellations toward -infinity; the PS5's gfx1013 is not measured. A result beyond 65504 becomes infinity or 65504 as IEEE 754 gives for the mode. Flushed f16 denormals still throw, and packed f16 throws with any f16 rounding but nearest even. `FP16_OVFL` set is not measured. -- [Signaling NaN inputs](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) of the f32 and f16 unary instructions (`v_sqrt/rcp/rsq/log/exp/sin/cos/fract/floor/ceil/trunc/rndne/rcp_iflag/frexp_mant`), `v_ldexp_f32/f16`, `v_frexp_mant_f16`, `v_div_fixup_f32/f16` and `v_mullit_f32` are returned quieted only with IEEE=1 and unchanged with IEEE=0, measured on RDNA2 (gfx1035) in both modes, and so are those of the f64 forms (`v_sqrt/rcp/rsq/fract/floor/ceil/trunc/rndne/frexp_mant/ldexp/div_fixup_f64`) and `v_cvt_f64_f32`, measured on RDNA2 (gfx1036) in both modes; shaders without a known float mode follow IEEE=0. `v_pack_b32_f16` quiets each signaling NaN half by the same rule, measured on RDNA2 (gfx1036) in both modes. A signaling NaN result of `v_cubesc/cubetc/cubema_f32` is quieted by the same rule and keeps the sign the instruction gives it, measured on RDNA2 (gfx1036) in both modes. `v_add/sub/subrev/mul/fma/fmac/fmamk/fmaak/mul_legacy_f32` and `v_fma_mix_f32` with f32 sources choose their NaN result on the bits, measured over 1000 operand triples in both modes: the first NaN source in operand order after its modifiers (the subtrahend negated, as the hardware adds its negation), quieted only with IEEE=1, or the default NaN 0xffc00000 for an invalid operation; in the fma forms an invalid product (inf * 0) gives the default NaN even when src2 is a NaN, and in the legacy forms a zero factor makes the product 0 even when the other factor is a NaN (a NaN src2 still wins). `v_mad_legacy_f32` and `v_mac_legacy_f32` follow the same rule with their separately rounded product, as measured on gfx1010 (#1281, #1290): a NaN source quieted only with IEEE=1, inf - inf giving 0xffc00000, the zero rule hiding a NaN factor and a NaN src2 winning. The f16 and f64 arithmetic, `v_fma_mix_f32` with f16 sources and the separately rounded mad/mac forms (not measurable on RDNA2) follow the same rule on hardware but their NaN bits are left to the driver. `v_cvt_f16_f32`, `v_cvt_f32_f64`, `v_dot2_f32_f16`, `v_fma_mixlo_f16` and `v_fma_mixhi_f16` always quiet and `v_div_scale_f32` never does: the f32 signaling NaN 0x7f812345 as src0 or src2 gives 0x7e09 from mixlo and mixhi at IEEE=0 and IEEE=1 on RDNA2 (gfx1036), where `v_fma_mix_f32` gives 0x7f812345 at IEEE=0 and 0x7fc12345 at IEEE=1. The payload of an f16 NaN source of mixlo and mixhi is left to the driver's f16 to f32 conversion, which NVIDIA replaces. +- [Signaling NaN inputs](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) of the f32 and f16 unary instructions (`v_sqrt/rcp/rsq/log/exp/sin/cos/fract/floor/ceil/trunc/rndne/rcp_iflag/frexp_mant`), `v_ldexp_f32/f16`, `v_frexp_mant_f16`, `v_div_fixup_f32/f16` and `v_mullit_f32` are returned quieted only with IEEE=1 and unchanged with IEEE=0, measured on RDNA2 (gfx1035) in both modes, and so are those of the f64 forms (`v_sqrt/rcp/rsq/fract/floor/ceil/trunc/rndne/frexp_mant/ldexp/div_fixup_f64`) and `v_cvt_f64_f32`, measured on RDNA2 (gfx1036) in both modes; shaders without a known float mode follow IEEE=0. `v_pack_b32_f16` quiets each signaling NaN half by the same rule, measured on RDNA2 (gfx1036) in both modes. A signaling NaN result of `v_cubesc/cubetc/cubema_f32` is quieted by the same rule and keeps the sign the instruction gives it, measured on RDNA2 (gfx1036) in both modes. `v_add/sub/subrev/mul/fma/fmac/fmamk/fmaak/mul_legacy_f32` and `v_fma_mix_f32` with f32 sources choose their NaN result on the bits, measured over 1000 operand triples in both modes: the first NaN source in operand order after its modifiers (the subtrahend negated, as the hardware adds its negation), quieted only with IEEE=1, or the default NaN 0xffc00000 for an invalid operation; in the fma forms an invalid product (inf * 0) gives the default NaN even when src2 is a NaN, and in the legacy forms a zero factor makes the product 0 even when the other factor is a NaN (a NaN src2 still wins). `v_mad_legacy_f32` and `v_mac_legacy_f32` follow the same rule with their separately rounded product, as measured on gfx1010 (#1281, #1290): a NaN source quieted only with IEEE=1, inf - inf giving 0xffc00000, the zero rule hiding a NaN factor and a NaN src2 winning. The f16 and f64 arithmetic, `v_fma_mix_f32` with f16 sources and the separately rounded mad/mac forms (not measurable on RDNA2) follow the same rule on hardware but their NaN bits are left to the driver, except that an invalid operation of `v_fma_f16`, `v_fmac_f16`, `v_fmamk_f16`, `v_fmaak_f16` and `v_fma_mix_f32` with f16 sources (an inf * 0 product, also with a NaN src2, or a NaN from sources that are not NaN) gives the default NaN 0xfe00 or 0xffc00000, which NVIDIA does not return on its own. Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) at IEEE=0 and IEEE=1 with f16 and f32 denormals kept and flushed, over six inf * 0 products with eight NaN src2 values in every f16 fma form (#7074); the AMD Windows driver 32.0.21042.62 on an RDNA2 iGPU (Ryzen 5 7600) instead returns src2 quieted through Vulkan in the f16 fma forms. `v_cvt_f16_f32`, `v_cvt_f32_f64`, `v_dot2_f32_f16`, `v_fma_mixlo_f16` and `v_fma_mixhi_f16` always quiet and `v_div_scale_f32` never does: the f32 signaling NaN 0x7f812345 as src0 or src2 gives 0x7e09 from mixlo and mixhi at IEEE=0 and IEEE=1 on RDNA2 (gfx1036), where `v_fma_mix_f32` gives 0x7f812345 at IEEE=0 and 0x7fc12345 at IEEE=1. The payload of an f16 NaN source of mixlo and mixhi is left to the driver's f16 to f32 conversion, which NVIDIA replaces. - [Scalar f16 add, sub, subrev and mul denormals](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follow the `FLOAT_MODE` f16 denormal field at nearest-even: bit 0 preserves inputs and bit 1 preserves the result; otherwise a denormal is flushed to its signed zero. Measured on RDNA2 (gfx1036) with IEEE=0/1 and FP16_OVFL=0/1, over 32 operand pairs in all four denormal modes, including signed zeros, denormals, minimum normals, infinities and NaNs. Multiply detects tininess after rounding to 11 significant bits with an unbounded exponent, before gradual half packing: `0x3bff * 0x0400` flushes, while `0x3c01 * 0x03ff` rounds to `0x0400` and is kept. `v_fma_f16`, `v_fmac_f16`, `v_fmamk_f16` and `v_fmaak_f16` follow the same field at nearest-even: inputs are flushed when bit 0 is clear, and the result is flushed when bit 1 is clear if the exact product-sum rounded to 11 significant bits with an unbounded exponent is below 2^-14, so `0x0400 * 0x0400 + 0x8400` rounds to `0x8400` and is kept. Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) at IEEE=0 and IEEE=1 with denorm16 0..3 over 5075 operand triples of `v_fma_f16` and `v_fmac_f16` (4000 from 20 halves around the denormal range, 75 whose product-sum lies just below 2^-14 and 1000 random), and over the first 4075 of them for `v_fmamk_f16` and `v_fmaak_f16`, FP16_OVFL=1, the clamp, neg and abs modifiers and op_sel. Directed rounding and `v_fma_mixlo/mixhi_f16` keep their previous denormal lowering. Shaders without a known float mode keep the previous lowering. Scalar `v_sub_f16`/`v_subrev_f16` share `float16Binary` with add/mul and were measured on gfx1036 over the same operand set as add (1156 non-NaN-input rows per mode); NaN result bits still depend on the driver as recorded above. - [f16 unary, frexp and conversion denormals](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follow the `FLOAT_MODE` f16 denormal field: `v_ceil_f16`, `v_floor_f16`, `v_fract_f16`, `v_rcp_f16`, `v_rsq_f16`, `v_sqrt_f16`, `v_log_f16`, `v_exp_f16`, `v_sin_f16`, `v_frexp_mant_f16`, `v_frexp_exp_i16_f16`, `v_cvt_norm_i16_f16`, `v_cvt_norm_u16_f16` and `v_cvt_f32_f16` flush a denormal half input to its signed zero when bit 0 is clear (0 and 2), and the f16-result ones flush a denormal half result to its signed zero when bit 1 is clear (0 and 1), after the host rounds it to half. Measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1 with denorm16 0..3 over every non-NaN f16 input (63,490 rows per mode and instruction): the only mismatches are the 1-ULP driver differences of `v_exp_f16` (2 inputs) and `v_sin_f16` (14), which are the same with denormals kept. NaN inputs are not compared. `v_ldexp_f16` shares the input rule. `v_cvt_pknorm_i16_f16` and `v_cvt_pknorm_u16_f16` flush both half inputs by the same bit, measured on RDNA2 (gfx1030) over every f16 input (each half also negated in the other source) at IEEE=0 and IEEE=1 with denorm16 0..3 and at f16 rounding 1..3: every row matches. Shaders without a known float mode keep the previous lowering. - [v_div_fixup_f16](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) flushes its src1 and src2 to a zero of the same sign when bit 0 of the `FLOAT_MODE` f16 denormal field is clear (0 and 2) and keeps a denormal src0 and result in every mode, as `v_div_fixup_f32` does with the f32 field. Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) over 6859 triples of 19 halves (zeros, denormals, minimum normals, ±0.5, ±1, the largest finite value, infinities and quiet and signaling NaNs) at IEEE=0 and IEEE=1 with denorm16 0..3 and FP16_OVFL 0 and 1, also with neg and abs source modifiers and with op_sel high halves. Shaders without a known float mode keep the previous lowering. The PS5's gfx1013 is not measured. @@ -108,14 +118,14 @@ - [v_ldexp_f32 in denormal modes 1 and 2](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follows the `FLOAT_MODE` f32 denormal field: mode 1 keeps a subnormal input and flushes a result below 2^-126 to a zero of its sign in every f32 round mode, also one that rounds to 2^-126 at subnormal precision (`0x3fffffff` scaled by 2^-127); mode 2 flushes a subnormal input to a zero of its sign and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, as mode 3 does. Measured on RDNA2 (gfx1030, Radeon RX 6800, AMD Windows driver 32.0.21045.5002 through HIP) over 10,260 rows (54 inputs of both signs: zeros, subnormals, normals near 2^-126 and 1.0, the largest finite value, infinities and NaNs, each scaled by 190 exponents from -2^31 to 2^31-1) at IEEE=0 and IEEE=1 with each f32 denormal mode 0 to 3 and f32 round mode 0 to 3, DX10_CLAMP=1, f16 denormals kept, f16 round to nearest even and FP16_OVFL=0; with the `clamp` modifier the result is clamped as in the other modes. The PS5's gfx1013 is not measured. - [v_log_f32 and v_log_f16](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) keep the driver's log2 unless, for inputs within 1/8 of 1.0, it is more than 4 ULP from an eight-term series in x - 1, which then replaces it. `v_log_f16` goes through the same f32 lowering and is rounded to half. The series is evaluated for every `v_log` and chosen with `OpSelect` rather than a branch, so its seven fused multiply-adds and two multiplies are an unconditional cost. Measured on RDNA2 over all 3,145,728 f32 inputs within 1/8 of 1.0 (gfx1035, IEEE 0, f32 denormals flushed), the hardware is within 1 ULP of the correctly rounded log2 and the series within 2 ULP of it and within 3 ULP of the hardware (2 except at 0x3f752e47, 0x3f7baab0 and 0x3f7bc4fb), so a kept driver value is within 7 ULP of the hardware and a driver that returns the hardware's bits never triggers the gate; RADV (Mesa 26.2.4) returns the hardware's bits on the 9,280 such inputs measured on gfx1036; NVIDIA (615.71.09) was up to 23,947,836 ULP off at 1 - 2^-24 and is within 6 ULP inside the band after the change; outside it its own log2 stays up to 9 ULP off (measured up to 1.375). Over every f16 input, the change moves one `v_log_f16` result on NVIDIA (0x3bed, 1 ULP, onto the hardware's bits) and none on RADV. - [Ordered not-equal, less-than and greater-than compares](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) (shader recompiler) - `FPOrdNotEqual32` is emitted as `OpFUnordNotEqual`, and `FPOrdLessThan32` and `FPOrdGreaterThan32` as their ordered forms, each with both operands checked for NaN on their bits; the results are the same for every input. The AMD Windows driver 32.0.13058.8001 (Radeon 860M, RDNA 3.5) returns 1 for a NaN operand when any float form of not-equal feeds a ballot on a 32-wide subgroup (#825), and so does a less-than or greater-than against an infinity the driver sees as a constant (#3536), presumably rewritten into a not-equal by its compiler -- [16-bit integer multiplies](../../../core/shader/recompiler/Translation/src/IntegerInstructions.cpp) (`v_mul_lo_u16`, `v_pk_mul_lo_u16`) (shader recompiler) - a factor taken from the low half of its source is read as the low 24 bits of the register, not the low 16; the low 16 bits of the product are the same for every input. The AMD Windows driver 32.0.21045.5002 (RX 6700 XT, RDNA 2) stores the full 32-bit product when two values it knows fit in 16 bits are multiplied, the product is reduced to 16 bits and nothing else is merged into the stored value: `v_mul_lo_u16` into a register whose high half is zero, also with factors masked to 16 bits by the program, and `v_pk_mul_lo_u16` whose high lanes are zero wrote 0xfffe0001 for 0xffff * 0xffff where the GPU writes 0x00000001. Reducing the product with `v_and_b32`, `v_bfe_u32` or a shift pair loses the same bits; factors read as 24-bit fields do not. Measured against the same instructions run through the [hardware oracle](../HW_ORACLE.md) on that GPU; other drivers are not checked +- [16-bit integer multiplies](../../../core/shader/recompiler/Translation/src/IntegerInstructions.cpp) (`v_mul_lo_u16`, `v_pk_mul_lo_u16`) (shader recompiler) - a factor taken from the low half of its source is read as the low 24 bits of the register, not the low 16; the low 16 bits of the product are the same for every input. The AMD Windows driver 32.0.21045.5002 (RX 6700 XT, RDNA 2) stores the full 32-bit product when two values it knows fit in 16 bits are multiplied, the product is reduced to 16 bits and nothing else is merged into the stored value: `v_mul_lo_u16` into a register whose high half is zero, also with factors masked to 16 bits by the program, and `v_pk_mul_lo_u16` whose high lanes are zero wrote 0xfffe0001 for 0xffff * 0xffff where the GPU writes 0x00000001. Reducing the product with `v_and_b32`, `v_bfe_u32` or a shift pair loses the same bits; factors read as 24-bit fields do not. The [constant folder](../../../core/shader/recompiler/Optimization/src/ConstantFolder.cpp) keeps a field read that a multiply uses, even when the program already masked the factor to 16 bits, since folding it away brings the full product back (#8271). Measured against the same instructions run through the [hardware oracle](../HW_ORACLE.md) on that GPU; other drivers are not checked - [f32 compares in denormal modes 1 and 2](../../../core/shader/recompiler/Translation/src/VectorInstructions.cpp) (`v_cmp_X_f32` and `v_cmpx_X_f32` with X = eq, ge, gt, le, lg, lt, neq, nge, ngt, nle, nlg, nlt) flush their f32 denormal inputs by bit 0 of the `FLOAT_MODE` f32 denormal field: mode 1 keeps them as mode 3 does, and mode 2 flushes them as mode 0 does. Measured on RDNA2 (gfx1030, Radeon RX 6800, AMD Windows driver 32.0.21045.5002 through HIP) over 484 operand pairs of zeros, denormals, minimum normals, ones, the largest finite value, infinities and quiet and signaling NaNs of both signs, in VOPC form, at IEEE=0 and IEEE=1 with each f32 denormal mode 0 to 3, f32 round to nearest even, DX10_CLAMP=1, f16 denormals kept, f16 round to nearest even and FP16_OVFL=0. The PS5's gfx1013 is not measured. - [f16 compares](../../../core/shader/recompiler/Translation/src/CompareInstructions.cpp) (`v_cmp_X_f16` and `v_cmpx_X_f16` with X = eq, ge, gt, le, lg, lt, neq, nge, ngt, nle, nlg, nlt) flush their f16 denormal inputs to a zero of the same sign by bit 0 of the `FLOAT_MODE` f16 denormal field: modes 0 and 2 flush them, 1 and 3 keep them. Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) over 256 operand pairs of zeros, denormals, minimum normals, ones, the largest finite value, infinities and quiet and signaling NaNs of both signs, in VOPC form, at IEEE=0 and IEEE=1 with each f16 denormal mode 0 to 3, f16 round to nearest even, DX10_CLAMP=1, f32 denormals kept and FP16_OVFL=0. Shaders without a known float mode keep the previous lowering. The PS5's gfx1013 is not measured. - [DB_SHADER_CONTROL.PRIMITIVE_ORDERED_PIXEL_SHADER](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - the host pixel interlock spans the whole shader, not only up to `s_sendmsg` ORDERED_PS_DONE. - [POPS collision SGPRs](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - the collision wave ID and intrawave collision mask read 0, so a title skips its POPS wait and runs its per-collision loop once. - [Image loads](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) (shader recompiler) - an image_load reads a sampled binding, also of an image the shader stores to, so in a primitive-ordered pixel shader it misses an earlier overlapping primitive's image_store in the same draw. - [Tessellation rings](../../../core/shader/recompiler/Optimization/src/TessellationLowering.cpp) of local, hull and domain programs become Vulkan tessellation interfaces and are never written to guest memory; a title that reads the off-chip or factor ring back sees no data. -- [Hull input control points](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - a hull LDS read at a control-point index computed from data reads `gl_in` at that index clamped to the patch's last input control point. On the console an index past the patch reads the LDS slot of a neighbouring patch or of no patch; the host has no such memory, and Vulkan leaves an out-of-range `gl_in` index undefined. The lowering refuses a computed offset that is not a whole number of control points, but the index itself is only known when the program runs. +- [Hull input control points](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - a hull LDS read at a control-point index computed from data reads `gl_in` at that index clamped to the patch's last input control point, and a domain off-chip read at a computed index reads `gl_in` clamped to the patch's last output control point. On the console an index past the patch reads the LDS slot or off-chip record of a neighbouring patch or of no patch; the host has no such memory, and Vulkan leaves an out-of-range `gl_in` index undefined. The lowering refuses a computed offset that is not a whole number of control points, but the index itself is only known when the program runs. - [Inner tessellation factors](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) of integer, fractional-even or fractional-odd triangle patches that are NaN tessellate as 1 (fractional-even then raises them to 2, as Vulkan does), as in the D3D11 reference tessellator; the console is not measured. - [Hull programs](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) start from the hull launch registers (user data, ring bases, v0, v1); on the console they start with what the local program leaves, and that it leaves them unchanged is not checked. - [Quad tessellation factors](../../../core/shader/recompiler/Optimization/src/TessellationLowering.cpp) are taken as outer[0..3] then inner[0..1], 24 bytes per patch, as [Mesa stores them](https://github.com/mihawk-99/PS5_Mesa/blob/b3588f78dd7fd46fbadc7ecd487e5f14d4213e4b/src/amd/common/nir/ac_nir_lower_tess_io_to_mem.c); unmeasured on the console. @@ -124,7 +134,7 @@ - [Integer partitioning](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (`VGT_TF_PARAM.PARTITIONING` 0) is drawn with Vulkan's equal spacing, which [radeonsi programs as `PART_INTEGER`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-24.0.0/src/gallium/drivers/radeonsi/si_state_shaders.cpp#L539-540), with factors bounded at 64 instead of fractional-odd's 63; how the console rounds a clamped factor up to a whole number of segments is not measured. Power-of-two (1) partitioning throws. - [Fractional-even partitioning](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (`VGT_TF_PARAM.PARTITIONING` 3) is drawn with Vulkan's fractional-even spacing, which [radeonsi programs as `PART_FRAC_EVEN`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-24.0.0/src/gallium/drivers/radeonsi/si_state_shaders.cpp#L536-537), with factors bounded at 64. Vulkan raises every factor that is not culled to at least 2 after `VGT_HOS_MIN/MAX_TESS_LEVEL`, so a `VGT_HOS_MAX_TESS_LEVEL` below 2 still tessellates at 2; the D3D11 range for this mode is also [2, 64], but the console's order of the HOS clamp and that minimum is not measured. - The [relative patch index](../../../core/shader/recompiler/Optimization/src/TessellationLowering.cpp) that hull and domain programs use as data is (PrimitiveID mod PRIM_GRP_SIZE) mod NUM_PATCHES, with the draw-relative instance times the patches per instance added when GE_CNTL.BREAK_WAVE_AT_EOI is clear and the instances share hull threadgroups; a domain that uses it in such a draw, and a hull without shaderDrawParameters, throw. Unmeasured on hardware; PPSA26344 programs 64 primitives for 63 patches. -- The [output modifier](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) (`mul:2`, `mul:4`, `div:2`) follows the shader's float mode as measured on RDNA2 (gfx1035): the hardware applies it only with IEEE mode off and output denormals of the result's precision flushed (bit 1 of the `FLOAT_MODE` denormal field clear), and ignores it otherwise. On f32 results it is applied as the hardware does with f32 denormals flushed; a shader without a known float mode is translated with IEEE mode off and f32 denormals flushed, the mode titles use. On f16 and f64 results it is ignored, and it throws in the modes where the hardware applies it. +- The [output modifier](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) (`mul:2`, `mul:4`, `div:2`) follows the shader's float mode as measured on RDNA2 (gfx1035): the hardware applies it only with IEEE mode off and output denormals of the result's precision flushed (bit 1 of the `FLOAT_MODE` denormal field clear), and ignores it otherwise. On f32 results it is applied as the hardware does with f32 denormals flushed; a shader without a known float mode is translated with IEEE mode off and f32 denormals flushed, the mode titles use. On f16 and f64 results it is ignored, and it throws in the modes where the hardware applies it. `v_cubeid_f32`, `v_cubesc_f32`, `v_cubetc_f32` and `v_cubema_f32`, measured on RDNA2 (gfx1036) with `DX10_CLAMP`=1, apply it whenever IEEE mode is off, in all four f32 denormal modes (`v_mul_f32_e64` in the same measurement follows the rule above), with the same bit-level result as the f32 path, and ignore it with IEEE mode on; clamp is applied after it. Only round to nearest even is modelled: in the other round modes (measured with IEEE mode off) gfx1036 rounds an overflow as the mode says (to the largest finite value when rounding toward zero), in `v_cubema_f32`'s own result and in the `mul:2` and `mul:4` scaling, where the translation gives infinity, and when rounding toward negative infinity a zero `v_cubema_f32` result with a NaN input is -0, where the translation gives +0. - The [clamp modifier](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follows `DX10_CLAMP`, measured on RDNA2 (gfx1035) for f32, f16, packed f16 and f64 results at IEEE=0 and IEEE=1: with `DX10_CLAMP`=1 (and in shaders without a known float mode) a NaN result clamps to +0, including the quieted NaN that `v_pk_min_f16` and `v_pk_max_f16` return for a signaling NaN operand with IEEE=1 (measured on RDNA2 gfx1036 at both `IEEE_MODE` and both `DX10_CLAMP` settings); with `DX10_CLAMP`=0 a NaN result is written unchanged and every other value clamps to [0, 1] as before. f64 results follow the same rule on hardware, but a clamped f64 result with `DX10_CLAMP`=0 throws until it has an execution test. With `DX10_CLAMP`=0 the NaN's bits are those the translation already produces for the instruction without clamp, so where those are left to the driver (f32 and scalar f16 arithmetic) the payload is the driver's, not the hardware's input propagation. `v_dot2_f32_f16` and the integer clamps are unchanged. - [FP16_OVFL](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follows the shader's float mode as measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1 with f16 denormals kept and flushed: with `FP16_OVFL`=1 an f16 result that rounds to ±infinity is written as ±0x7bff unless a source operand is infinite, so `v_rcp_f16`, `v_rsq_f16` and `v_log_f16` of ±0 and `v_exp_f16` of 16 or more are clamped while an infinite input passes through, and `v_div_fixup_f16` keeps the infinity it writes for a zero denominator or an infinite numerator and clamps any other infinite quotient, a NaN one included. `v_cvt_pkrtz_f16_f32` rounds toward zero and never overflows. The sources are checked as the f32 values the translation computes with; the rule is applied to `v_fma_mix*_f16` with f32 sources and to `v_interp_p2_f16` without a measurement of infinite f32 sources or parameters. Round modes other than nearest even are not modelled, so the overflow threshold is the nearest-even one (65520). - [Directed f16 rounding](../../../core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp) for `v_cvt_f16_f32`, `v_cvt_f16_i16`, `v_cvt_f16_u16` and `v_ldexp_f16` follows `FLOAT_MODE` bits 3:2 in every f16 denormal mode, measured on RDNA2 (gfx1036 with f16 denormals preserved, gfx1030 in all sixteen combinations of f16 rounding and denormal mode). When bit 7 flushes f16 results, `v_cvt_f16_f32`, `v_cvt_f16_i16` and `v_cvt_f16_u16` write a zero of the result's sign for a value that, rounded in that mode with an unbounded exponent, is below 2^-14, so a value just below 2^-14 that rounds up to it stays 0x0400; `v_ldexp_f16` keeps its rule above. The `clamp` modifier is applied before the rounding. On gfx1030 every row matches: 8,996 f32 inputs around the half denormal, normal and overflow boundaries with and without `clamp`, every 16-bit integer, and 2,470 `v_ldexp_f16` operands. @@ -143,7 +153,7 @@ - [Raw `buffer_load_dwordx2`](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) is one 64-bit access on the hardware (with or without glc), so it never sees half of a `buffer_atomic_swap_x2`. Only a glc load that is 8-byte aligned and in bounds, in a program that has the 64-bit storage view (it uses 64-bit buffer atomics), is read with one 64-bit atomic load; every other dwordx2 load is two dword loads and can tear against a concurrent 64-bit write: loads without glc, and loads in a program without 64-bit buffer atomics while another shader writes the pair with `swap_x2`. - [Buffer loads without GLC or DLC](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) are not coherent in a program that has GLC or DLC buffer accesses and no `buffer_gl0_inv`, `buffer_gl1_inv`, `s_gl1_inv` or `s_dcache_inv`: only its GLC and DLC loads, stores and atomics use the `Coherent` buffer view, so a plain load can return a value cached before another work-group's write. This follows the [LLVM AMDGPU memory model for GFX10](https://llvm.org/docs/AMDGPUUsage.html#memory-model-gfx10-gfx11), in which a load-acquire is a GLC DLC load followed by `buffer_gl1_inv` and `buffer_gl0_inv`; it is not measured on the console or on RDNA hardware. - [DS src2 operations](../../../core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp) (`ds_*_src2_*`, GFX10.1 only) read `DS[B]`, with `B = A + 4 * sext(offset1[6:0]:offset0)`, and then update `DS[A]` with the matching 32-bit atomic, so the read of `B` is not atomic with the update of `A`. The form that takes the offset from the address (`offset1[7]` set), the 64-bit forms and GDS throw: whether the LDS address keeps `A[31:17]` in that form, and whether the 64-bit forms scale the offset by 4 or 8, is not documented. `ds_min_src2_f32`, `ds_max_src2_f32` and `ds_add_src2_f32` throw: on RDNA2 (gfx1035, the oracle's hardware, which has no src2 opcodes) `ds_min_f32`, `ds_max_f32` and `ds_add_f32` ignore `IEEE_MODE` but depend on whether f32 denormals are flushed (28, 28 and 12 of 196 special rows differ), and that is not modelled for LDS float atomics yet (#981). -- [`ds_ordered_count`](../../../core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp) (GDS only) reads its operand fields as LLVM encodes them for `llvm.amdgcn.ds.ordered.add` ([SIISelLowering.cpp](https://github.com/llvm/llvm-project/blob/llvmorg-19.1.0/llvm/lib/Target/AMDGPU/SIISelLowering.cpp#L8844-L8894)): `offset0` is the counter's byte offset (index times 4) and the whole GDS address, `offset1` bit 0 wave release, bit 1 wave done, bits 2-3 the shader type, bit 4 swap instead of add and bits 6-7 the dword count less one. `M0` holds the ordered wave ID, not a GDS address as for `ds_append` (radeonsi passes `TG_SIZE` bits 6-17 as the intrinsic's pointer in [si_compute_prim_discard.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-20.3.0/src/gallium/drivers/radeonsi/si_compute_prim_discard.c) and `gs_tg_info` bits 0-11 in [gfx10_shader_ngg.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-21.3.0/src/gallium/drivers/radeonsi/gfx10_shader_ngg.c), with "The GDS address is always 0 with ordered append"); it only orders the waves, which is not modeled, so it is ignored. Each wave adds the value of its first active lane to the counter with one device-scope atomic and every active lane gets the counter's previous value (the `ORDERED_APPEND_ENBL` dispatch modifier is accepted for it). The hardware also serializes the waves of a dispatch in launch order, which is not modeled: the waves add in any order, so counters a title reads back as ordered offsets give the same totals but may hand out ranges in another order. Which lane's value is added is not documented; the first active lane is assumed and not measured on hardware. Swap, counts of more than one dword and LDS forms throw. Covered by [GdsAppendConsume.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/GdsAppendConsume.cpp). +- [`ds_ordered_count`](../../../core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp) (GDS only) reads its operand fields as LLVM encodes them for `llvm.amdgcn.ds.ordered.add` ([SIISelLowering.cpp](https://github.com/llvm/llvm-project/blob/llvmorg-19.1.0/llvm/lib/Target/AMDGPU/SIISelLowering.cpp#L8844-L8894)): `offset0` is the counter's byte offset (index times 4) and the whole GDS address, `offset1` bit 0 wave release, bit 1 wave done, bits 2-3 the shader type, bit 4 swap instead of add and bits 6-7 the dword count less one. `M0` holds the ordered wave ID, not a GDS address as for `ds_append` (radeonsi passes `TG_SIZE` bits 6-17 as the intrinsic's pointer in [si_compute_prim_discard.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-20.3.0/src/gallium/drivers/radeonsi/si_compute_prim_discard.c) and `gs_tg_info` bits 0-11 in [gfx10_shader_ngg.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-21.3.0/src/gallium/drivers/radeonsi/gfx10_shader_ngg.c), with "The GDS address is always 0 with ordered append"); it only orders the waves, which is not modeled, so it is ignored. Each wave adds the value of its first active lane to the counter with one device-scope atomic and every active lane gets the counter's previous value (the `ORDERED_APPEND_ENBL` dispatch modifier is accepted for it). The hardware also serializes the waves of a dispatch in launch order, which is not modeled: the waves add in any order, so counters a title reads back as ordered offsets give the same totals but may hand out ranges in another order. Which lane's value is added is not documented; the first active lane is assumed and not measured on hardware. A swap (`offset1` bit 4) stores the first active lane's value in place of the counter with one device-scope atomic exchange and returns the counter's previous value to every active lane; with the waves unordered, which wave's value the counter keeps and which previous value each wave gets need not follow the launch order the hardware gives. Counts of more than one dword and LDS forms throw. Covered by [GdsAppendConsume.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/GdsAppendConsume.cpp). - [s_swappc_b64](../../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) writes the link pair with the same model as `s_getpc_b64`: the guest return address `pc + 4` as a byte offset over `GetShaderBase`. Any other use of the link pair (arithmetic, hash, store) throws, apart from the overwrites, SOPC compares and shared links that [static scalar calls](../../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) allow; constant-offset folding over the link is not modeled. The special encoding `s_swappc_b64` with SDST 125 still decodes as `s_setpc_b64`, which matches the hardware: measured on RDNA2 (gfx1036) in wave32 and wave64, it branches to the source pair and leaves a marker SGPR pair untouched, and with an SGPR destination the link is `PC + 4`. - [2D biased sampling through a one-slice, single-level 3D T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) supplies a zero third coordinate and retains the bias operation, with normalized point minification and magnification and no linear mip filtering. Measured on RDNA2 (AMD integrated gfx1036, driver 32.0.21036.18, Windows 11) by running an OpenCL-loaded native kernel with `image_sample_b` DIM 2D against 16x16x1 R32_UINT and RGBA8_UNORM 3D images: biases -16, -0.5, 0, 0.5 and 16 read the (u,v) texel in slice zero, including wrapped coordinates, point min/max reduction and the title's sampler with Z border addressing. Wave32, IEEE off, DX10 clamp on, denormals preserved, round to nearest even, FP16 overflow off; wave64 and the PS5's gfx1013 are not measured. Pixel-stage, direct, non-depth, unpacked samples whose only use is the plain 32-bit bias form are accepted; extra slices, any nonzero mip range, linear or anisotropic filtering and unnormalized coordinates throw. Vulkan readback is checked on NVIDIA RTX 5070 and AMD integrated graphics for positive and negative bias and point weighted-average/min/max reduction. - [FORCE_DEGAMMA on RGBA8_UINT](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) materializes a direct image sampled only by plain biased pixel accesses as RGBA8_SRGB when every paired sampler forces degamma. Measured on AMD integrated RDNA2 gfx1036 (driver 32.0.21036.18, Windows 11, wave32, IEEE off, DX10 clamp on, denormals preserved, round to nearest even, FP16 overflow off): a native `image_sample_b` DIM 2D on a 16x16x1 RGBA8_UINT 3D T# returns integer channels without the bit, but the same floating-point decoded values as RGBA8_UNORM with it, including point min/max reduction and the title's exact sampler. The metadata and image view must both become floating-point sRGB; ignoring the sampler bit or retaining a UINT heap is incorrect. The accepted case requires one slice, mip zero, normalized point sampling and uniform FORCE_DEGAMMA across its samplers. Other image accesses, mixed sampler states, indirect descriptors and other integer formats remain refused. Wave64, other filtering modes and the PS5's gfx1013 are not measured. @@ -153,12 +163,16 @@ - [PERSP_SAMPLE and LINEAR_SAMPLE](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (recompiler) are the barycentrics at the iterated sample, which in a pixel shader the guest does not iterate is taken to be sample 0, inferred from the ancillary's iterated sample number and not measured. They are interpolated with `InterpolateAtOffset`, which [GLSL_EXT_fragment_shader_barycentric](https://github.com/KhronosGroup/GLSL/blob/0099bf83b0288469917fc2e2972cb0a2b74d6711/extensions/ext/GLSL_EXT_fragment_shader_barycentric.txt#L286-L299) names for barycentrics away from the default position, at the sample's offset from the pixel centre, read per pixel of the 2x2 quad from `PA_SC_AA_SAMPLE_LOCS` (the standard pattern unless the draw sets its own): sample 0's in a shader the guest does not iterate, and in an iterated shader the offset of the host's `SampleId` from a table of the guest's locations, because Vulkan leaves the position of plain barycentrics under sample shading implementation-defined (lavapipe uses the pixel centre). That needs the device's `sampleRateShading`; without it, and when every location is the pixel centre (a single-sample draw), they keep the host's default position, the pixel centre once per pixel and an implementation-defined position under sample shading. PERSP_CENTER and LINEAR_CENTER are interpolated at offset 0 in an iterated shader, so they stay at the pixel centre as on the console; the centroid pairs keep the host's default position. With barycentric emulation the offsets apply to the emulated barycentrics. Devices that interpolate attributes on the host instead (no usable fragment-shader barycentrics, as on MoltenVK) keep the default position for all four pairs. - [Pixel positions](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (recompiler) - POS_X_FLOAT and POS_Y_FLOAT follow `SPI_BARYC_CNTL.POS_FLOAT_LOCATION` (bits 17:16) and `POS_FLOAT_ULC` (bit 20), whose positions come from [PAL's GFX9+ register header](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/chip/gfx9_plus_merged_registers.h) and whose meanings from [LLPC](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/lgc/lowering/RegisterMetadataBuilder.cpp#L767-L779): the pixel centre (0), the iterated sample (2), or the upper-left corner (ULC). Under host sample shading the centre is `floor(FragCoord) + 0.5`, and a non-iterated shader's sample is the centre plus sample 0's offset. The centroid (1) keeps `FragCoord`, the pixel centre or under sample shading the sample, not the centroid of the covered samples. A missing `SPI_BARYC_CNTL` reads as the pixel centre and the reserved location 3 throws. POS_Z_FLOAT and POS_W_FLOAT keep `FragCoord`'s depth and 1/w at the host's position. `PERSP_CENTER_CNTL`, `PERSP_CENTROID_CNTL`, `LINEAR_CENTER_CNTL` and `LINEAR_CENTROID_CNTL` (bits 0, 4, 8 and 12) are not read: PAL and LLPC never set them. - [LINE_STIPPLE_TEX](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (recompiler) reads as 0: line stipple is not emulated. RADV enables the input only as the required pixel input of a shader that reads no interpolants, because it initializes the fewest VGPRs ([Mesa](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/vulkan/radv_shader.c) `radv_compute_spi_ps_input`), and never reads it -- [Static scalar calls](../../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) (`s_call_b64` and `s_swappc_b64`) require an ordinary even-aligned SGPR link pair (s0:s1 through s104:s105) and a later `s_setpc_b64` return reading that exact pair. A pair serves one call, or several calls whose regions are disjoint, each closed by the first return reading the pair after its target. Outside every region of its calls the pair is free to use wherever no return address can reach: a dataflow over the decoded instructions follows each dword of the pair from the calls that write it until a scalar load, SALU or VALU result is known to overwrite it (a VALU mask counts as one SGPR unless the request says wave64), and an instruction there that may read a dword still holding a return address throws; SOPC compares are exempt, as they only set SCC. Regions must nest or be disjoint; recursion, other shared links, escaping link values (including overlapping tuples and resource descriptors), invalid targets, interleaved regions, cross-region branches or fall-through, relative SGPR reads and dynamic `s_setpc_b64` in a program with calls throw. The analysis checks all decoded instructions, including unreachable code. Arbitrary indirect calls and general call frames are not implemented. +- [Static scalar calls](../../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) (`s_call_b64` and `s_swappc_b64`) require an ordinary even-aligned SGPR link pair (s0:s1 through s104:s105) and a later `s_setpc_b64` return reading that exact pair. Outside every region of its calls the pair is free to use wherever no return address can reach: a dataflow over the decoded instructions follows each dword of the pair from the calls that write it until a scalar load, SALU or VALU result is known to overwrite it, and an instruction there that may read a dword still holding a return address throws; SOPC compares are exempt, as they only set SCC. A VALU mask covers one SGPR in wave32 and a pair in wave64. When the graph is built without the wave size, the dataflow runs for both: a program that escapes under both throws there, and one that escapes under only one throws when it is translated at that size. +- [Static scalar call link pairs](../../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) each serve one call, or several calls whose regions are disjoint, each call closed by the first return reading the pair after its target. Regions must nest or be disjoint; recursion, other shared links, escaping link values (including overlapping tuples and resource descriptors), invalid targets, interleaved regions, cross-region branches or fall-through, relative SGPR reads and dynamic `s_setpc_b64` in a program with calls throw. The analysis checks all decoded instructions, including unreachable code. Arbitrary indirect calls and general call frames are not implemented. - [Relative vector addressing](../../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) (`v_movrels_b32`, `v_movreld_b32`, `v_movrelsd_b32`, `v_movrelsd_2_b32`, `v_swaprel_b32`) keeps every vector register from the lowest base register the program addresses relatively up to the last register the program names in zero-initialised function-scope storage per invocation (`LoadVectorWindow`/`StoreVectorWindow`, one element per register), so an indexed access is one load or store instead of a compare-and-select chain over the window. [`IndexedWindowSlots`](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp) bounds each relative index through the IR (bit-field extracts, masks, shifts, adds, selects, phis, `ReadFirstLane` and the window stores that reach a load); the storage is an array of the slots below the largest bound that is under half the window, and one function variable per slot after it. A constant index uses the array element or the slot's variable; a relative access whose index has no such bound, or lands past the array at run time, indexes the array below its size and otherwise branches through an `OpSwitch` over the slot variables, so the bound only decides speed. When no index has a bound under half the window, the whole window is the array, as with `APS5_NO_WINDOW_NARROWING=1` (a recompiler switch in the shader cache key). Measured on an NVIDIA GeForce RTX 3060 (driver 615.78.08, Linux) with a compute program whose 167-register window an ALU loop reads and writes in full and whose relative accesses take `m0 = s8 & 3`: a 2048-group dispatch took 70.2 ms in wave32 and 123.3 ms in wave64 with the whole window in the array (as before narrowing) and 1.9-2.1 ms and 7.8 ms narrowed; with a second relative access whose index has no bound and lands past the array at run time (index 50 or 166), 113 ms and 5.8-6.0 ms in wave32. In the PPSA08427 (Killing Floor 3) hub it made no measurable difference to presents or to the GPU time of the eight most expensive programs. Whether the host compiler keeps the array and the slot variables in registers or spills them to scratch is not measured. A register in the window the program never wrote reads 0, as an unwritten SSA register does; the console's value is undefined. M0 is unsigned: an index at or past the window reads `v0` and drops its write, and `v_swaprel_b32` writes neither side when either is out of range, as measured on RDNA2 (gfx1035, `agc_driver_relative_indexing`). The window ends at the last register the program names rather than at the shader's VGPR allocation, which the recompiler does not receive, so a relative write past the named registers but inside the allocation is dropped where the hardware keeps it for a later relative read. Killzone: Liberation's PSP vertex program (PPSA11142, 3176 ISA words, 104 relative accesses over 154 registers) went from 649,515 to 392,333 SPIR-V words with this. Pixel programs whose relative span contains the packed ancillary input instead keep that span in SSA and implement indexed operations with compare/select chains. This keeps ancillary definitions visible to field lowering and masked-write proofs; storing the unmodelled whole word in a private array would require composing unknown bits. A fold after masked-select elimination lowers fields whose consumers become visible only after redundant copies are removed. A whole-word use that the masked-select pass keeps gets the composed word described under the packed pixel ancillary word. Both representations include implicit entry inputs in the inferred register limit and retain unsigned M0, inactive-write and DPP update rules; wave32/wave64 native Vulkan readbacks cover relative reads, writes, split-offset swaps, inactive writes, DPP masks and out-of-range accesses, but console comparison and the performance cost of selecting over this span are unverified. - [s_call_b64](../../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp) saves `PC + 4` over `GetShaderBase` and branches to `PC + 4 + signext(SIMM16) * 4`, following [AMD RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) sections 3.6.3, 12.2 and 12.3. Forward callees after `s_endpgm` are decoded. Measured on RDNA2 (gfx1036) in wave32 and wave64 with SIMM16 = 3: the link pair receives `PC + 4`, the branch goes to `PC + 4 + signext(SIMM16) * 4`, and `s_setpc_b64` of the link returns to the instruction after the call. Execution on the PS5 (gfx1013) and title compatibility for these bounded scalar calls remain unverified; `s_rfe_b64` still throws in translation. +- [BDA anchor halfword reads](../../../core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp) retain volatile byte loads even for noncoherent memory. On RX 6650 XT / RADV Mesa 26.2.4, the valid SPIR-V for two adjacent nonvolatile byte loads returned `0x80a0` for bytes `0xa0, 0xa1`, where lookup-based reads returned `0xa1a0`. Adding the volatile operand makes the same GPU exactness fixture pass, including mixed direct/fallback lanes. The driver transformation responsible has not been isolated; removing this restriction needs equivalent readback coverage on the affected driver. Single-byte reads keep their original access mask. - [BDA span reads](../../../core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp) (shader recompiler) - `read_bda_span` is one selection: one arm calls `load_bda_span`, a `DontInline` function that holds the in-place dword loads and calls nothing, and the other arm is the per-dword byte fallback. Two other shapes of the same reads each hang a group of NVIDIA drivers, where a dispatch never finishes and the device is lost. With the loads emitted in the arm itself, a dispatch whose lanes took both arms hung on an RTX 3060 and an RTX 4060 Laptop GPU (Windows, driver 591.86) and an RTX 5060 (Linux, driver 590.48.01) ([#3291](https://github.com/boykopovar/AnyPS5/issues/3291)). With the loads and the fallback as two selections, the first under the span's condition and the second under its negation, it hung on an RTX 4070 Laptop GPU (Windows, drivers 595.97 and 596.36), an RTX 5080 (Windows, driver 610.62) and an RTX 4090 (Linux, driver 610.57.04) ([#4975](https://github.com/boykopovar/AnyPS5/issues/4975)). The current shape is measured on the RTX 4090 with driver 610.57.04 only, where it does not hang with or without SPIRV-Tools; a shape with the loads in their own `DontInline` function was reported passing on driver 591.86 in #3291. The cause inside the driver is not known. - [SDWA source selects on `v_cvt_f32_ubyte1`, `v_cvt_f32_ubyte2` and `v_cvt_f32_ubyte3`](../../../core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp) (shader recompiler) - byte N is taken from source 0 after `SRC0_SEL` and `SRC0_SEXT` select and extend it, since SDWA supplies source 0 (RDNA 2 ISA guide 6.8 and 13.3.7) and the opcode reads `S0.u[8N+7:8N]` (12.8). Not measured on hardware. - [Flag structurization](../../../core/shader/recompiler/ControlFlow/src/Structurizer.cpp) (shader recompiler) - a graph that every structurizer attempt refuses, irreducible and non-terminating graphs included, is rebuilt with one reach flag per block and one again flag per loop (`structurizeWithFlags`): each block keeps one copy and runs behind a guard on its flag, and each strongly connected component becomes a loop whose single latch repeats it while a back edge set its flag. The output has about 8 times the input's blocks and one route variable per block, and ends in an empty Return that never runs. Only blocks that start the top-level order unconditionally run unguarded, so resource planning also sees the static paths that skip a guarded block and can plan extra descriptor candidates for them. The attempts still run first. Shared merge splitting refuses an attempt once the graph has grown past 16 times its block count at the start of splitting (at least 64 blocks); succeeding attempts grew at most 10.6 times on random graphs of up to 128 blocks, and an attempt that would succeed only beyond the budget leaves the graph to a later attempt or to this construction. Splitting that adds one block per round stays within that budget until its 4 rounds per block run out, so the attempts can still take more than 10 s (9 of 2,000 random graphs of up to 128 blocks, 63 s at most). `APS5_FORCE_STRUCTURIZER_FALLBACK=1` builds every graph this way, for tests - [Scalar buffer descriptors](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) use only the base address, stride and record count for resources accessed exclusively by scalar loads; their fourth word is normalized to zero, following AMD RDNA2 ISA section 7.2.2. Vector and mixed accesses retain descriptor validation. Native Vulkan readbacks cover ignored fourth-word fields on Windows/NVIDIA; their console behavior has not been measured. - [Spanned wave64 compute](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp) (shader recompiler) - `APS5_SPANNED_WAVE=1`, off by default, runs a single-wave wave64 compute program at one lane per invocation on a 32-lane host subgroup and exchanges ballots, shuffles and reductions between the two host subgroups through workgroup memory and barriers. The driver turns it on only where compute subgroups can be pinned at 32 full lanes (`subgroupSizeControl` and `computeFullSubgroups`), passes it as `SpirvTarget::spannedWaves` (part of the cache key and the serialized request), and creates the pipelines of spanned programs with `REQUIRE_FULL_SUBGROUPS`, since the lane number `SubgroupId * 32 + SubgroupLocalInvocationId` needs two full subgroups. Those barriers are correct only where every invocation of the workgroup reaches them, which holds for the guest's wave-uniform control flow. The emulated 64-bit LDS atomics ([LockedLdsUpdate](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp)) run only on the lanes the exec mask keeps, so their lock loop ballots within its own host subgroup, as its elect already does. Any other cross-lane exchange emitted inside per-lane control flow would hang a spanned wave in the same way, and the emitter does not reject one: there is no uniformity check. The only check so far is manual: disassemble the module (`spirv-dis`), list the selection and loop constructs that enclose each `OpControlBarrier`, and read the condition of each enclosing selection. In Ghost of Yōtei's program 0x80003af600 (PPSA26344), 108 of 111 barriers sat only in loops on wave-uniform conditions and 3 inside per-invocation conditionals, all in the lock loop. Other programs and titles have not been scanned. An earlier, cheaper ballot (one barrier instead of three) also lost the device for a reason that is not understood, which may be another barrier placement problem. Measured on an RTX 4090 (Windows): an offline replay of that program and two Yōtei runs that previously lost the device (NVIDIA TDR) now run past it, and [LdsMaskWaterfall.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/LdsMaskWaterfall.cpp) runs both modes and covers a half-inactive wave. Stray (PPSA02101) on Intel ANV and a Demon's Souls (PPSA01342) request replay showed no fault, before this fix -- [Guest memory load guards](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - a guest load whose lane is inactive or whose address is out of bounds is still performed and its result replaced by 0 with `OpSelect`, instead of a selection per guard, when the load can be pointed at a location nothing writes: scratch reads its own element 0; storage buffers (direct buffers, GDS) read element 0xfffffffe, which `robustBufferAccess2` turns into a zero load with no memory access, so the extra read cannot race with a write the guarded program would not have raced with ([Vulkan memory model](https://docs.vulkan.org/spec/latest/appendices/memorymodel.html#data-race)). Without `robustBufferAccess2` ([VulkanDevice.cpp](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) enables it when the device has it) those loads keep their selections; LDS loads, stores, atomics, formatted and physical-address (BDA) accesses always do. LDS keeps the branch because NVIDIA's compiler predicates a branched LDS load at no cost, while the select form costs about 2.5 instructions per dword and splits a `ds_read_b128` into four loads (RTX 4060 measurements by @karanvora2599 in #4048); Intel's static counts favour the select only for wide reads, and run time was not measured on either. The aligned high word of a misaligned-capable buffer read re-reads the low word's own element. A selection guard costs four blocks per lane half and the host compiler's time grows with the block count: PPSA11142's merged PSP vertex program (3176 ISA words, wave64) emitted 16,983 blocks and 1,710 conditional branches, 5,907 and 868 with the selects. `agc_guarded_memory_loads` traces the emitted loads in both device modes. AMD's proprietary driver does not get `robustBufferAccess2` ([RobustBufferAccess.hpp](../../../core/libs/prx/libSceAgcDriver/Execution/include/RobustBufferAccess.hpp)), so its loads keep their selections. On an RX 6700 XT with driver 32.0.21045.5002, enabling the feature broke `agc_driver_layered_volume_draw` and `agc_driver_layered_volume_vertex`: the vertex draw left the volume untouched even with the selections kept. `agc_driver_subvector_loop` failed too, with the selected loads: the high half of a wave64 skipped its loop. The cause was not found. +- [Guest memory load guards](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - a guest load whose lane is inactive or whose address is out of bounds is still performed and its result replaced by 0 with `OpSelect`, instead of a selection per guard, when the load can be pointed at a location nothing writes: scratch reads its own element 0; storage buffers (direct buffers, GDS) read element 0xfffffffe, which `robustBufferAccess2` turns into a zero load with no memory access, so the extra read cannot race with a write the guarded program would not have raced with ([Vulkan memory model](https://docs.vulkan.org/spec/latest/appendices/memorymodel.html#data-race)). Without `robustBufferAccess2` ([VulkanDevice.cpp](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) enables it when the device has it) those loads keep their selections; LDS loads, stores, atomics, formatted and physical-address (BDA) accesses always do. LDS keeps the branch because NVIDIA's compiler predicates a branched LDS load at no cost, while the select form costs about 2.5 instructions per dword and splits a `ds_read_b128` into four loads (RTX 4060 measurements by @karanvora2599 in #4048); Intel's static counts favour the select only for wide reads, and run time was not measured on either. The aligned high word of a misaligned-capable buffer read re-reads the low word's own element. A selection guard costs four blocks per lane half and the host compiler's time grows with the block count: PPSA11142's merged PSP vertex program (3176 ISA words, wave64) emitted 16,983 blocks and 1,710 conditional branches, 5,907 and 868 with the selects. `agc_guarded_memory_loads` traces the emitted loads in both device modes. AMD's proprietary driver does not get `robustBufferAccess2` ([RobustBufferAccess.hpp](../../../core/libs/prx/libSceAgcDriver/Execution/include/RobustBufferAccess.hpp)), so its loads keep their selections. On an RX 6700 XT with driver 32.0.21045.5002, enabling the feature broke `agc_driver_layered_volume_draw` and `agc_driver_layered_volume_vertex`: the vertex draw left the volume untouched even with the selections kept. `agc_driver_subvector_loop` failed too, with the selected loads: the driver compiled the begin instruction's `exec_lo == 0` as a test of the whole 64-bit mask, so the high half of a wave64 skipped its loop. The begin instruction now tests for an empty mask as `(exec_lo | exec_hi) == 0` rather than `exec_lo == 0 && exec_hi == 0`, and that driver compiles it correctly. The cause of the layered-volume failures was not found. +- [Merged hull programs](../../../core/shader/recompiler/Optimization/src/TessellationLowering.cpp) (registered binary type 3) run whole in the hull stage, one invocation per output control point. On the console hull lane i keeps local lane i's registers, which with fewer input than output control points stop matching the patch's control points from i = inputs on; here invocation i holds control point min(i, inputs - 1) of its own patch. A wave-mask branch (`s_cbranch_exec[n]z`, `s_cbranch_vcc[n]z`) tests the invocation's own lane, so it can skip a block, scalar instructions included, that the console runs for the wave; `v_readfirstlane_b32` returns the invocation's own value, not the first active lane's, and another lane's register throws. The register layout (`s3`, `v0`-`v5`, as for the separate halves) is not measured on hardware. s[0:1] hold the hull user-data address (SH 0x102/0x103) when a draw sets it and zero otherwise, so a program reading it unset faults at the null address. +- [Wave LDS ordering on AMD's proprietary driver](../../../core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp) (shader recompiler) - there ([SubgroupBarrier.hpp](../../../core/libs/prx/libSceAgcDriver/Execution/include/SubgroupBarrier.hpp)) one guest wave's LDS accesses are ordered by a subgroup-scope `OpMemoryBarrier` with no control barrier, relying on a 32-lane subgroup being one hardware wave whose lanes run in lockstep and issue LDS instructions in order, which Vulkan does not guarantee; for LDS kept in device memory the same is assumed of global loads and stores, unmeasured. Driver 32.0.21042.62 (Radeon 0x13c0, RDNA 2) compiled the subgroup-scope `OpControlBarrier` to `s_barrier`, which waves in a branch paired with other waves' barriers (PS5LM Plus). Measured on that driver only ([LdsTreeReduction.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/LdsTreeReduction.cpp)); a Radeon 860M on 32.0.13058.8001 reportedly did not show the fault. diff --git a/docs/user/INPUT_MAPPING.md b/docs/user/INPUT_MAPPING.md index 74221f95ab..7ab80c3d4f 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: @@ -79,6 +80,22 @@ RightStickDeadzone = 8 MouseSensitivity = 2.5 ``` +## Controller timelines + +Set `APS5_PAD_INPUT_FILE` as described in [Controller input for test runs](USAGE.md#controller-input-for-test-runs), then queue an entire button sequence after the application begins reading its controller. Save a JSON file such as `menu.json`: + +```json +{"version":1,"events":[{"at_ms":200,"hold_ms":300,"buttons":["cross","r1"]},{"at_ms":1000,"hold_ms":300,"buttons":["down"]}]} +``` + +```sh +python3 tools/pad_timeline.py menu.json /tmp/anyps5-pad --repeat 3 --gap-ms 500 +``` + +Events start relative to when the application first observes the new command. Button lists support every name in `pad_input.py`; simultaneous names are combined. An empty list or `["release"]` injects no buttons. Events must be ordered and non-overlapping, hold for 1–5000 ms, and finish within one hour. Repetition adds the specified gap after the last event; at most 4096 events are accepted after repetition. The helper validates the entire sequence before atomically replacing the command file. + +The runtime evaluates scheduled events at each controller read, including their automatic releases. A new pulse or timeline replaces the remaining sequence. Physical controller buttons still add to injected buttons. Existing commands at the application's first controller read are ignored. This schedules wall-time input: events can be missed while the application is not reading its controller, and it does not reproduce frame timing or game state. Use gaps and holds long enough for the title's input loop. + ## Analog keyboard input `StickModifier` has no built-in binding. While a key or mouse button bound to it is held, the keyboard stick bindings tilt the stick by `StickModifierScale` of its range instead of fully, for walking or fine aim. Controller sticks and the mouse stick are not affected, and opposite keys still cancel each other. diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 8dac97ffac..42270f520c 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -63,7 +63,7 @@ All switches are disabled by default. `unused-filter` defaults to `0`; `--rpath` | `--windows-diagnostics` | Include startup dependency diagnostics. Requires `--windows`. | | `--windows-gui` | Select the Windows GUI subsystem instead of the console subsystem. Requires `--windows`. | | `--windows-fixed-base
` | Map the lowest ELF load page at this fixed Windows address, given in decimal or hexadecimal (`0x`). Requires `--windows`, a 64 KiB-aligned address from `0x20000` through `0x7fffffff0000`, and a complete image fitting in the user address space. The main executable has no ASLR or base relocations; an occupied address fails to load. Bundled modules keep their normal placement. | -| `--trace-imports` | Pass each function import through the libc.prx call tracer, see [tracing system library calls](#tracing-system-library-calls). Requires `--windows`; conflicts with `--lazy-binding`. | +| `--trace-imports` | Pass each function import through the libc.prx call tracer, see [tracing system library calls](#tracing-system-library-calls). Windows and Linux output only; conflicts with `--lazy-binding`. | | `--linux-fixed-base
` | Load the Linux executable at this fixed address, given in decimal or hexadecimal (`0x`): every virtual address of the image is offset by it, so virtual address 0 is placed at the address, as the console places an executable. Requires a 64 KiB-aligned address from `0x10000` through `0x7fffffff0000` that is a multiple of every `PT_LOAD` alignment, and an image that ends in the user address space. The executable is written as `ET_EXEC`, without ASLR. Bundled modules keep their normal placement. Conflicts with `--windows`, `--macos` and `--lazy-binding`. | | `--macos-fixed-base
` | Map the lowest ELF load page of the macOS executable at this fixed address, given in decimal or hexadecimal (`0x`). Requires `--macos`, a 64 KiB-aligned address from `0x20000` through `0x7fffffff0000`, and an image that ends in the macOS user address space. `__PAGEZERO` ends 64 KiB below the address and the Mach-O header occupies those 64 KiB. The executable has no ASLR; an address the process cannot map fails to load. Bundled modules keep their normal placement. | | `--to-intel` | Convert supported AMD-only instructions in the executable and bundled modules. Unsupported instructions or unreachable conversion stubs cause an error. | @@ -166,7 +166,7 @@ app0/ ``` -Use `sce_modules/` or `prx/` instead of `sce_module/` if that is the input directory name. Relinker preserves each module's directory under `app0/` and prints its exact path. Place app resources in `app0/` separately, including the title's original `eboot.bin`: titles test `/app0/eboot.bin` to find their own directory, as it is always present on the console (ProsperoTV, ProsperoRadio, ProsperoLight and ProsperoLichess fall back to an empty `/data/homebrew/` and fail to load their fonts without it). Only the converted executable runs. Copy the built system libraries from `build/core/libs/libs/*.prx` into `libs/`; use libraries built for the target OS. A custom `--rpath` changes the system library location. +Use `sce_modules/` or `prx/` instead of `sce_module/` if that is the input directory name. Relinker preserves each module's directory under `app0/` and prints its exact path. Place app resources in `app0/` separately, including the title's original `eboot.bin`: titles test `/app0/eboot.bin` to find their own directory, as it is always present on the console (ProsperoTV, ProsperoRadio, ProsperoLight and ProsperoLichess fall back to an empty `/data/homebrew/<title id>` and fail to load their fonts without it). Only the converted executable runs. On Linux, `app0/` must not contain symbolic links: a directory listing gives the title the link's own type, not the type of the file or directory it points to, so a title that walks its directories skips a linked directory. Use copies or hard links. Copy the built system libraries from `build/core/libs/libs/*.prx` into `libs/`; use libraries built for the target OS. A custom `--rpath` changes the system library location. Some applications also need their original `app0/sce_sys/param.json` at runtime. AnyPS5 reads this file for the title ID and display name; it must contain a non-empty `titleId` string and a non-empty `localizedParameters` object with a language entry that has a non-empty `titleName`. It selects the entry named by `defaultLanguage` (inside `localizedParameters`, or at the top level); if that entry is absent, it tries `en-US`, then the first object-valued language entry. Preserve the values from the title's own metadata and place the file under the same `app0/` directory as the other app resources. At startup, its presence in the working directory creates `data/`; a non-zero `downloadDataSize` also creates `download0/`. Without the file, neither directory is created. If it is missing or invalid, metadata calls such as title/window setup can fail with a `param.json` error. @@ -200,6 +200,10 @@ A Windows executable started without a standard output or error handle, such as The game chooses its video-output resolution; AnyPS5 uses that mode for rendering. The display window can be resized or switched to fullscreen, which scales the presented image to fit the window. Changing the window size does not change the game's render resolution. AnyPS5 does not currently provide a global render-resolution override; use a game's own display setting when it offers one. +### VR compatibility + +AnyPS5 does not currently support PS VR2 headsets or PC VR headsets connected through an OpenXR runtime. It does not provide headset tracking, stereoscopic output or reprojection. A VR title may skip its headset setup when no device is detected, but that does not mean its VR gameplay is supported. + ### Controller input for test runs Set `APS5_PAD_INPUT_FILE` to an absolute path before launching the application to enable controller pulses from a local command file. Once the application is reading controller input, send a button with the helper from the repository root: @@ -214,11 +218,17 @@ python3 tools/pad_input.py /tmp/anyps5-pad cross --left-stick 255 128 --right-st python3 tools/pad_input.py /tmp/anyps5-pad touch-right ``` +For timed button sequences, see [Controller timelines](INPUT_MAPPING.md#controller-timelines). + Run the helper in a second terminal. Commands are polled at most every 100 ms during controller reads, and their buttons add to physical controller input. A press lasts 300 ms by default, starting when the application reads it, and releases automatically; `--hold-ms` accepts 1–5000 ms. `--left-stick X Y` and `--right-stick X Y` hold the sticks for the same time: each axis is 0-255, 128 is the centre, X grows to the right and Y grows downward. While such a command is active both sticks come from it instead of the physical controller, and a stick that was not named stays centred. `touch-left` and `touch-right` press the touchpad with a finger on its left or right half, as the `TouchLeft` and `TouchRight` keys do; in the command file they are bits `0x200000` and `0x400000` of the button field. Wait for the application to observe a release before sending another press. The file holds only the latest command; rapid commands can replace one another. On POSIX, replacing an existing command file retains its permission mode; a privileged helper also retains its owner and group so the game user can still read it. Commands left over when the application first reads the controller are ignored. Without the environment variable, file input is disabled. +### Video-out and pad telemetry + +Set `APS5_VIDEOOUT_TELEMETRY_DIR` to a directory before launching the application to write the run's presents and pad changes to `<directory>/videoout.jsonl`, one JSON object per line, appended when the file already exists. Every line starts with `"v":1`, the host's steady-clock nanoseconds in `t_ns` and the event name in `ev`. A run starts with a `contract` line, `{"v":1,"t_ns":<ns>,"ev":"contract","contract":"v1-pad","commit":"<12 hex>","pid":<pid>,"prx":"libSceVideoOut"}`: `commit` is the source commit the libraries were built from (`unknown` when Git was not found at configuration) and `pid` tells apart the runs that appended to the same file. A `pad` line is written when the pad state the title reads changes in its buttons, sticks or triggers, `{"v":1,"t_ns":<ns>,"ev":"pad","n":<present>,"buttons":<bits>,"sticks":[<left x>,<left y>,<right x>,<right y>],"triggers":[<L2>,<R2>]}`, where `n` is the number of presents completed when the change was published, the count the window title shows. `buttons` is the button bit field in decimal, stick axes run 0-255 with 128 at the centre, and both triggers run 0-255. Motion, touch and the device kind are not compared, so a run that only moves the controller writes no `pad` line. The state a `pad` line reports is the controller and keyboard/mouse input read through SDL; input fed by `APS5_PAD_INPUT_FILE` is applied later, in `libScePad`, so a scripted run writes no `pad` line. A `present` line is written after the first present and every 30 presents after that, `{"v":1,"t_ns":<ns>,"ev":"present","n":<present>,"fps":<rate>}`, with the count and rate the window title shows. A background thread writes the lines and the title is not waited for: when it falls behind, the line is dropped and a `drops` line with the number dropped so far, `{"v":1,"t_ns":<ns>,"ev":"drops","dropped":<count>}`, is written every ten seconds while the count grows, and once at a normal exit, only when lines were dropped; an exit that skips static destructors writes no last line. A pad line the full queue refused counts as dropped even though a later publish writes the change. Without the variable no file is opened and no thread is started; an empty value is the same as an unset one, and a directory that cannot be written stops the title when video output starts, with an error naming the file. + ### System fonts -Games that open the console's system font sets (`sceFontOpenFontSet`) need font files in an `anyps5-fonts/` directory beside the output executable; set `ANYPS5_SYSTEM_FONTS` to use another directory. Files dumped from the console are used under their own names (`SST-Roman.otf`, `SST-Bold.otf`, `SSTJpPro-Regular.otf`, ...). Without them, these openly licensed substitutes are used when present: `NotoSans-{Light,Regular,Medium,Bold}.ttf` and `NotoSans-{LightItalic,Italic,MediumItalic,BoldItalic}.ttf` (Latin and Vietnamese), `NotoSansMono-{Light,Regular,Medium,Bold}.ttf` (typewriter), `NotoSansThai-{Light,Regular,Medium,Bold}.ttf` (Thai) and `NotoSansCJK-{Light,Regular,Medium,Bold}.ttc` (Japanese and Chinese). Without either, opening a system font set fails and the game shows no text in those fonts. +Games that open the console's system font sets (`sceFontOpenFontSet`) need font files in an `anyps5-fonts/` directory beside the output executable; set `ANYPS5_SYSTEM_FONTS` to use another directory. Files dumped from the console are used under their own names (`SST-Roman.otf`, `SST-Bold.otf`, `SSTJpPro-Regular.otf`, ...). Without them, these openly licensed substitutes are used when present: `NotoSans-{Light,Regular,Medium,Bold}.ttf` and `NotoSans-{LightItalic,Italic,MediumItalic,BoldItalic}.ttf` (Latin and Vietnamese), `NotoSansMono-{Light,Regular,Medium,Bold}.ttf` (typewriter), `NotoSansThai-{Light,Regular,Medium,Bold}.ttf` (Thai), `NotoSansArabic-{Light,Regular,Medium,Bold}.ttf` (Arabic) and `NotoSansCJK-{Light,Regular,Medium,Bold}.ttc` (Japanese, Chinese and Korean). A set that covers several scripts also opens the files of its other scripts (`SSTThai-*.otf`, `SSTArabic-*.otf`, `SSTJpPro-*.otf`, `DFHEI5-SONY.ttf`, `SCEPS4Yoongd-*.otf` or their substitutes) for the characters its main file lacks; a missing one leaves those characters without a glyph. Without either, opening a system font set fails (the log shows `sceFontOpenFontSet` with `file=(none)`), and what happens next depends on the game: some show no text in those fonts, and others stop at start-up because they assert that the open succeeded. The substitutes come from the [Noto project](https://github.com/notofonts/notofonts.github.io) under the SIL Open Font License (the Noto Sans files are in `fonts/NotoSans/full/ttf`, the Arabic ones in `fonts/NotoSansArabic/full/ttf`); copy the files named above into `anyps5-fonts/`. ### Console language @@ -247,6 +257,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 `<title ID>-<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. @@ -270,7 +284,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 @@ -278,7 +292,7 @@ Besides the recompiled shaders, the shader cache directory holds `pipelines.jour ### Texture cache budget -Sampled textures and storage images stay in device memory in two caches that evict their least recently used entries when they grow past their budgets. The storage image budget is what the `VK_EXT_memory_budget` budget of the largest device-local heap leaves after the heap's use other than the cached storage images themselves, the part of the sampled textures' 2 GiB floor not yet in use, and an eighth of the budget; it is capped at a quarter of that budget, at most a quarter of the heap's size or 2 GiB, whichever is larger, and never below 512 MiB. The sampled texture budget is what the reported budget leaves after the heap's use other than cached textures, a quarter of the heap's size (at least 2 GiB) and an eighth of the budget, and never below 2 GiB. While the sampled textures alive stay within their floor the sampled budget falls first, to that floor, then the storage budget; above it both fall together. Each cache trims at its next insert. Both budgets are read again every second; `[gpu] sampled texture cache budget` lines report them at start-up and when either moves by more than a tenth, at most every 10 s. `APS5_TEXTURE_CACHE_MIB=<n>` fixes both budgets at `n` MiB, and the lines are then not printed. +Sampled textures and storage images stay in device memory in two caches that evict their least recently used entries when they grow past their budgets. The storage image budget is what the `VK_EXT_memory_budget` budget of the largest device-local heap leaves after the heap's use other than the cached storage images themselves and an eighth of the budget; it is capped at a quarter of that budget, at most a quarter of the heap's size or 2 GiB, whichever is larger, and never below 512 MiB. The sampled texture budget is what the reported budget leaves after the heap's use other than cached textures, a quarter of the heap's size (at least 2 GiB) and an eighth of the budget, and never below 2 GiB. As the heap's other use grows the sampled budget falls first, to that floor, then the storage budget; the storage budget also gives back what the sampled textures alive take as they grow. Each cache trims at its next insert. Both budgets are read again every second; `[gpu] sampled texture cache budget` lines report them at start-up and when either moves by more than a tenth, at most every 10 s. `APS5_TEXTURE_CACHE_MIB=<n>` fixes both budgets at `n` MiB, and the lines are then not printed. A fixed budget above the automatic one can exceed what the card holds next to the rest of the frame. Allocations then go to host memory (`[gpu] ... placed in host memory` lines), where the GPU reads them over PCIe. PPSA16064 on an RTX 4060 8 GB, whose automatic storage image budget was 1797 MiB under the earlier rule, placed 868 and 878 MiB in host memory with `APS5_TEXTURE_CACHE_MIB=3072` and walked through its first level at 4.46 and 4.83 FPS; with the variable unset it placed none and ran the same walk at 6.48 and 6.07 FPS. Leave it unset unless a measurement on the card shows otherwise. @@ -288,13 +302,19 @@ 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. +Windows and Linux. An executable converted with `--trace-imports` records the calls it makes to the libraries named in `APS5_TRACE_CALLS`, a comma-separated list such as `libkernel,libSceSaveData`, or `all`. Without the variable nothing is recorded. The crash and abort reports then end with the last 64 calls (on Linux, which has no crash report, only an abort prints them): sequence number, thread, library, import name as the executable imports it (a NID) and the caller's address. The last 32 of these calls made by the reporting thread follow, taken from the 65536 most recent calls, so a thread whose calls were pushed out of the last 64 by a busier one still shows them. Then come the 32 most called imports since start-up, with their number of calls. ``` last traced system library calls (APS5_TRACE_CALLS): #1 thread 9384 libkernel.prx k+AXqu2-eBc from 0x0000000140010006 app.exe+0x10006 +last traced calls of thread 9384: + #1 libkernel.prx k+AXqu2-eBc from 0x0000000140010006 app.exe+0x10006 most called traced imports (1 calls in total): 1 libkernel.prx k+AXqu2-eBc ``` @@ -330,6 +350,8 @@ Set `APS5_CRASH_INDIRECT_MEMORY=1` before launching the executable to include up Set `APS5_CRASH_STACK_BYTES=<n>` to print, after the stack return address candidates, the faulting thread's stack words from `rsp` for `n` bytes (at most 1048576, decimal or `0x` hexadecimal), four words per line with their offset from `rsp`. Below them the report prints eight words at every distinct stack word that points at readable, non-executable memory outside the thread's stack, labelled with the word's offset and the address it holds, up to 256 addresses. This shows the objects the caller frames kept, such as the `this` of a destructor or the buffer of a container. The dump stops at the first unreadable stack page, and nothing is printed when the variable is unset, empty or 0. +The report lists every other thread of the process with its `rip`, `rsp`, `rbp` and the return addresses of its `rbp` frame chain. A thread whose chain yields fewer than two frames, such as one waiting inside `ntdll.dll` or in code built without frame pointers, also gets up to 24 stack return address candidates: the words in the 32 KiB above its `rsp` that point at executable memory, read while the thread is suspended. Like the candidates of the faulting thread they are a scan, not an unwind, so stale return addresses of finished calls can appear among them. + ### GPU submission rings and stall dumps Each GPU queue keeps its last 16 command buffer submissions: when each was submitted, its serial, size and guest address, and a copy of its first 512 dwords after indirect buffers are inlined. `APS5_PM4_RING=<n>` keeps the last `n` instead, from 0 to 4096; 0 keeps none, and any other value stops the title at its first submission. The rings are printed to stderr, one line per submission and one per decoded packet, when the Windows crash report runs: an unhandled exception, `abort()`, an invalid CRT parameter or the timed dump of `APS5_HANG_DUMP_SECS`. diff --git a/tools/build_web.py b/tools/build_web.py index cd19113603..66798820d2 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,89 @@ 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") + if output.is_symlink() or (output.exists() and not output.is_dir()): + raise ValueError(f"Release output is not a directory: {output}") + output.parent.mkdir(parents=True, exist_ok=True) + temporary = Path(tempfile.mkdtemp(prefix=".web-release-", dir=output.parent)) + staging = temporary / "site" + previous = temporary / "previous" + published = False + try: + staging.mkdir() + 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() + if output.exists(): + output.rename(previous) + try: + staging.rename(output) + except OSError: + if previous.exists(): + try: + previous.rename(output) + except OSError as error: + raise RuntimeError(f"Could not restore release output. Previous site is preserved at {previous}") from error + raise + published = True + finally: + if published or not previous.exists(): + shutil.rmtree(temporary) + 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 47076b046b..a251673abf 100644 --- a/tools/check_compile.py +++ b/tools/check_compile.py @@ -6,18 +6,33 @@ import shlex import shutil import subprocess import sys +from concurrent.futures import ThreadPoolExecutor +from contextlib import nullcontext 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: @@ -34,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) @@ -103,6 +118,9 @@ def syntax_only(entry, source): def syntax_arguments(arguments, directory, source): if not isinstance(arguments, list) or not arguments or any(not isinstance(argument, str) for argument in arguments) or not arguments[0]: raise ValueError("arguments must be a nonempty list of strings with an executable") + for argument in arguments[1:]: + if argument.startswith("@"): + raise ValueError(f"unexpanded response file {argument[1:]}; provide expanded compile arguments before checking syntax") wanted = source.resolve() trimmed = [] index = 0 @@ -111,7 +129,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("-"): @@ -127,33 +145,52 @@ def syntax_arguments(arguments, directory, source): return trimmed -def check(root, build, files): +def check_source(task): + source, name, build, configurations = task + if configurations is None: + return 0, 0, f"{name}: no compile command in {build}\n" + failed = False + diagnostics = [] + for entry in configurations: + command = (syntax_arguments(entry["arguments"], Path(entry["directory"]), source) + if "arguments" in entry else syntax_only(entry, source)) + result = subprocess.run(command, cwd=entry["directory"], + capture_output=True, text=True, encoding="utf-8", errors="replace") + if result.returncode == 0: + continue + failed = True + output = (result.stdout or "") + (result.stderr or "") + diagnostics.append(output if output.endswith("\n") else output + "\n") + return 1, int(failed), "".join(diagnostics) + + +def check(root, build, files, jobs=1): commands = index(build, root) - checked = 0 - failed = 0 + tasks = [] for file in files: source = file.resolve() - name = os.path.relpath(source, root).replace(os.sep, "/") - configurations = commands.get(name) - if configurations is None: - print(f"{name}: no compile command in {build}", file=sys.stderr) - continue - checked += 1 - source_failed = False - for entry in configurations: - command = (syntax_arguments(entry["arguments"], Path(entry["directory"]), source) - if "arguments" in entry else syntax_only(entry, source)) - result = subprocess.run(command, cwd=entry["directory"], - capture_output=True, text=True, encoding="utf-8", errors="replace") - if result.returncode == 0: - continue - source_failed = True - output = (result.stdout or "") + (result.stderr or "") - sys.stderr.write(output if output.endswith("\n") else output + "\n") - failed += int(source_failed) + try: + name = source.relative_to(root.resolve()).as_posix() + except ValueError: + name = source.as_posix() + tasks.append((source, name, build, commands.get(name))) + checked = failed = 0 + with (ThreadPoolExecutor(max_workers=jobs) if jobs > 1 else nullcontext()) as executor: + results = executor.map(check_source, tasks) if executor is not None else map(check_source, tasks) + for count, failures, diagnostics in results: + checked += count + failed += failures + sys.stderr.write(diagnostics) return checked, failed +def positive_integer(value): + number = int(value) + if number <= 0: + raise argparse.ArgumentTypeError("must be a positive integer") + return number + + def main(argv=None): for stream in (sys.stdout, sys.stderr): if hasattr(stream, "reconfigure"): @@ -162,6 +199,8 @@ def main(argv=None): description="compile the changed translation units only, to check a change builds without a full build") parser.add_argument("--build", type=Path, default=Path("build"), help="a configured Ninja build directory of this checkout (default: build)") + parser.add_argument("--jobs", type=positive_integer, default=1, metavar="N", + help="check up to N translation units concurrently; diagnostics retain input order (default 1)") parser.add_argument("--base", default="origin/main", help="branch the change is against (default: origin/main)") parser.add_argument("paths", nargs="*", help="translation units to check instead of the changed ones") @@ -172,7 +211,7 @@ def main(argv=None): if not files: print(f"no changed translation unit against {args.base}") return 0 - checked, failed = check(root, args.build, files) + checked, failed = check(root, args.build, files, args.jobs) except (json.JSONDecodeError, OSError, ValueError) as error: parser.error(str(error)) if failed: diff --git a/tools/check_conventions.py b/tools/check_conventions.py index c55fea49e7..132f275710 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 @@ -187,8 +206,7 @@ class Check: def duplicates(self, exports): if not exports: return - pattern = r"(^|[^[:alnum:]_])APS5_VABI[[:space:]]+(" + "|".join(exports) + r")[[:space:]]*\(" - result = subprocess.run(["git", "-c", "core.quotePath=false", "grep", "-nE", "--full-name", pattern, self.head, "--", ":(top)core/libs/prx"], capture_output=True, encoding="utf-8", errors="replace") + result = subprocess.run(["git", "-c", "core.quotePath=false", "grep", "-nF", "--full-name", "-f", "-", self.head, "--", ":(top)core/libs/prx"], input="\n".join(exports) + "\n", capture_output=True, encoding="utf-8", errors="replace") hits = [line.split(":", 3)[1:] for line in filter(None, result.stdout.split("\n"))] self.read(path for path, _, _ in hits) sources = {} @@ -388,7 +406,7 @@ def fenced_lines(text): def links(path, text): result = [] - text = INLINE.sub("", FENCE.sub("", text)) + text = INLINE.sub("code", FENCE.sub("", text)) for target in LINK.findall(text): if re.match(r"[A-Za-z][A-Za-z0-9+.-]*:|//|#", target): continue 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_skips.py b/tools/check_skips.py index 169cc7bc31..99628018e3 100644 --- a/tools/check_skips.py +++ b/tools/check_skips.py @@ -7,6 +7,8 @@ DEVICE_LIMITATION = re.compile(r"skipped,.*(subgroup|the device (has no|reports| NO_DISPLAY = re.compile(r"skipped,.*no display", re.IGNORECASE) +NO_VULKAN_DEVICE = re.compile(r"skipped,.*no usable Vulkan device", re.IGNORECASE) + HIGH_REFRESH_LIMITATION = re.compile(r"current display does not provide 120 Hz", re.IGNORECASE) WINDOWS_PREFIX = "agc" @@ -54,7 +56,7 @@ if __name__ == "__main__": print(f"{name}: {reason}") raise SystemExit if args.job == "windows": - unexpected = [name for name, output in found if not name.startswith(WINDOWS_PREFIX) and not NO_DISPLAY.search(output) and not high_refresh_limitation(output)] + unexpected = [name for name, output in found if not name.startswith(WINDOWS_PREFIX) and not NO_DISPLAY.search(output) and not NO_VULKAN_DEVICE.search(output) and not high_refresh_limitation(output)] expected = f"any {WINDOWS_PREFIX}* test, which the runner has no Vulkan driver for, or a test that reports no display or Vulkan device or a display without a 120 Hz mode" else: unexpected = [name for name, output in found if not DEVICE_LIMITATION.search(output) and not high_refresh_limitation(output)] diff --git a/tools/check_test_registration.py b/tools/check_test_registration.py index da156692b5..fcce86cbd1 100644 --- a/tools/check_test_registration.py +++ b/tools/check_test_registration.py @@ -11,6 +11,7 @@ LEXICAL = re.compile(r'\\.|"(?:\\.|[^"\\])*"|\[(?P<literal_equals>=*)\[.*?\](?P= r'|(?P<comment>\#\[(?P<comment_equals>=*)\[.*?\](?P=comment_equals)\]|\#[^\n]*)', re.S) KNOWN_UNRUN = set() +SCRIPT = re.compile(r"(?:[\w.-]+/)*test_\w+\.py") def built(build): @@ -53,8 +54,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(), [] @@ -124,13 +126,57 @@ def registered(build): return names +def listed(root): + output = subprocess.run(["git", "-c", "core.quotePath=false", "-C", str(root), "ls-files", "-z", "--cached", "--others", + "--exclude-standard"], capture_output=True, check=True).stdout.decode("utf-8", errors="replace") + return sorted(set(filter(None, output.split("\0")))) + + +def run_scripts(root, files): + run = set() + for name in files: + if name.endswith("CMakeLists.txt") or name.endswith(".cmake"): + content = (Path(root) / name).read_text(encoding="utf-8", errors="replace") + for command, arguments in commands(content): + if command == "add_test": + run.update(argument.rpartition("}")[2].lstrip("/") for argument in arguments) + elif name.startswith(".github/workflows/") and name.endswith((".yml", ".yaml")): + for line in (Path(root) / name).read_text(encoding="utf-8", errors="replace").splitlines(): + if line.lstrip().startswith("#"): + continue + line = re.split(r"\s#", line)[0] + if "python" in line: + run.update(token[2:] if token.startswith("./") else token for token in SCRIPT.findall(line)) + return {name for name in run if SCRIPT.fullmatch(name)} + + +def unrun_scripts(root): + files = listed(root) + run = run_scripts(root, files) + scripts = [name for name in files if re.match(r"^test_\w+\.py$", name.rpartition("/")[2])] + return scripts, [name for name in scripts if not any(name == path or name.endswith("/" + path) for path in run)] + + if __name__ == "__main__": parser = argparse.ArgumentParser(description="fail when a test executable is built that no ctest test runs") parser.add_argument("build", type=Path, nargs="?", default=Path("build"), help="configured build directory") parser.add_argument("--list", action="store_true", help="print every test executable and whether ctest runs it") + parser.add_argument("--scripts", action="store_true", help="instead check that an add_test or a workflow runs every test_*.py of the source tree") args = parser.parse_args() - every = sorted(built(args.build)) - run = registered(args.build) + if args.scripts: + scripts, unrun = unrun_scripts(Path(__file__).resolve().parents[1]) + for name in unrun: + print(f"error: {name} is a test script that no add_test or workflow runs") + if unrun: + print(f"{len(unrun)} test script(s) would never run: register each with add_test, or run it from a workflow") + raise SystemExit(1) + print(f"{len(scripts)} test script(s), each run by an add_test or a workflow") + raise SystemExit + 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 071d48e1a1..d1936ae21a 100644 --- a/tools/configure_seed.py +++ b/tools/configure_seed.py @@ -1,4 +1,5 @@ import argparse +import fnmatch import re import sys from pathlib import Path @@ -9,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 @@ -28,10 +29,24 @@ def main(): parser = argparse.ArgumentParser(description="Write the configure check results of a CMake cache as an initial cache file") parser.add_argument("cache", help="CMakeCache.txt of a configured tree") parser.add_argument("seed", help="file to pass to cmake -C") + parser.add_argument("--include", action="append", default=[], metavar="GLOB", + help="seed matching check names only; repeat to include several patterns") + parser.add_argument("--exclude", action="append", default=[], metavar="GLOB", + help="omit matching check names after inclusion; patterns are case-sensitive") args = parser.parse_args() - found = checks(args.cache) - 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") + 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") + try: + results = checks(cache) + except (OSError, UnicodeDecodeError) as error: + parser.error(f"{cache}: {error}") + found = {name: entry for name, entry in results.items() + if (not args.include or any(fnmatch.fnmatchcase(name, pattern) for pattern in args.include)) + and not any(fnmatch.fnmatchcase(name, pattern) for pattern in args.exclude)} + seed.parent.mkdir(parents=True, exist_ok=True) + seed.write_text("".join(f"set({quote(name)} {quote(value)} CACHE INTERNAL {quote(doc)})\n" + for name, (value, doc) in sorted(found.items())), encoding="utf-8") print(f"{len(found)} check results") return 0 diff --git a/tools/debt_split.py b/tools/debt_split.py index 6f8bfc054c..1460f46bca 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, []) @@ -115,7 +115,7 @@ def split(text): def render_index(index): out = [] for kind, item in index: - if kind == "heading" and out and out[-1] != "": + if kind in ("heading", "text") and out and out[-1] != "": out.append("") out.append(item) if kind == "heading" or kind == "text": @@ -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): @@ -197,7 +197,7 @@ def apply_change(root, base, new, index_text): for offset, fresh in enumerate(added): if offset in kept_new: continue - anchor = after[j1 + offset - 1] if j1 + offset > 0 else None + anchor = next((entry for entry in reversed(after[:j1 + offset]) if entry[1] == fresh[1] and entry[3] == fresh[3]), None) insert(fresh, anchor) for name, sections in files.items(): write_debt(root, name, sections, order) @@ -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..683b1e9168 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]+)')), @@ -76,7 +77,7 @@ def source_files(root): if not root.is_dir(): return for path in sorted(root.rglob("*")): - if path.suffix not in EXTENSIONS or any(part in SKIP for part in path.parts): + if path.suffix not in EXTENSIONS or any(part in SKIP for part in path.relative_to(root).parts): continue yield path @@ -171,6 +172,10 @@ def main(argv=None): args = parser.parse_args(argv) roots = args.root if args.root else ROOTS prefixes = tuple(args.prefix) if args.prefix else PREFIXES + for root in roots: + if not Path(root).is_dir(): + print(f"error: scan root {root} is not a directory", file=sys.stderr) + return 1 found = reads(roots, prefixes) if args.check: return check(found, args.doc, prefixes, sys.stdout) diff --git a/tools/hw-oracle/compare_results.py b/tools/hw-oracle/compare_results.py index 2ee1176024..dbc855f623 100644 --- a/tools/hw-oracle/compare_results.py +++ b/tools/hw-oracle/compare_results.py @@ -75,6 +75,13 @@ def compare(before, after): return differences +def nonnegative_integer(value): + number = int(value) + if number < 0: + raise argparse.ArgumentTypeError("must be a nonnegative integer") + return number + + def main(): parser = argparse.ArgumentParser( description="Compare two hw_oracle.py --json reports bit for bit without a GPU. " @@ -83,6 +90,8 @@ def main(): "Assembly targets do not identify the physical GPU or driver.") parser.add_argument("before", type=Path) parser.add_argument("after", type=Path) + parser.add_argument("--max-differences", type=nonnegative_integer, metavar="N", + help="print at most N differing words (0 prints only counts); all words are still compared") args = parser.parse_args() try: before, after = read_report(args.before), read_report(args.after) @@ -92,8 +101,11 @@ def main(): print(f"Assembly targets: {before['assembly_target']} -> {after['assembly_target']}") print(f"Compared {len(before['rows'])} rows x {len(before['output_registers'])} registers: " f"{len(differences)} differing words") - for row, register, old, new in differences: + visible = differences if args.max_differences is None else differences[:args.max_differences] + for row, register, old, new in visible: print(f"row {row}, {register}: 0x{old:08x} -> 0x{new:08x}") + if len(visible) < len(differences): + print(f"{len(differences) - len(visible)} additional differing words omitted") return int(bool(differences)) diff --git a/tools/hw-oracle/hw_oracle.py b/tools/hw-oracle/hw_oracle.py index 6b1fc4dddd..ffac30f5c7 100644 --- a/tools/hw-oracle/hw_oracle.py +++ b/tools/hw-oracle/hw_oracle.py @@ -166,7 +166,11 @@ def main(): parser.add_argument("--extra", type=Path, help="bytes appended after the rows") parser.add_argument("--outs", type=int, choices=range(1, 17), default=16, metavar="N", help="print v10..v(10+N-1)") parser.add_argument("--json", action="store_true", help="emit inputs, results and run settings as JSON; target is the assembly target, not a GPU model or driver") + parser.add_argument("--validate-only", action="store_true", + help="check input rows and report dispatch dimensions without assembling or executing the kernel") args = parser.parse_args() + if args.lds not in range(MAX_LDS + 1): + parser.error(f"lds must be an integer from 0 to {MAX_LDS}") try: rows = validated_rows(tuple(parse_word(v) for v in line.split()) for line in args.rows.read_text(encoding="utf-8").splitlines() if line.strip()) @@ -177,6 +181,18 @@ def main(): except (OSError, ValueError) as error: parser.error(str(error)) modes = {name: getattr(args, name) for name in ("ieee", "denorm32", "denorm16", "dx10_clamp", "round32", "round16", "fp16_overflow")} + if args.validate_only: + wave = 64 if args.wave64 else 32 + padded = (len(rows) + wave - 1) // wave * wave + dispatch = ROWS_PER_DISPATCH // 2 if args.wave64 else ROWS_PER_DISPATCH + report = {"validation_only": True, "rows": len(rows), "padded_rows": padded, + "dispatches": (padded + dispatch - 1) // dispatch, "wave_size": wave, + "floating_point_modes": modes, "lds_bytes": args.lds, + "memory": "coarse" if args.coarse else "fine", "extra_bytes": len(extra)} + print(json.dumps(report, indent=2) if args.json else + f"Validated {len(rows)} input rows, {padded} padded rows, {report['dispatches']} dispatches. " + "Assembly and GPU execution were not checked.") + return 0 results = run(body, rows, extra, args.wave64, args.coarse, **modes, lds=args.lds) if args.json: report = { diff --git a/tools/hw-oracle/test_compare_results.py b/tools/hw-oracle/test_compare_results.py index c2f18c7f6c..453ca40f9f 100644 --- a/tools/hw-oracle/test_compare_results.py +++ b/tools/hw-oracle/test_compare_results.py @@ -10,6 +10,7 @@ import unittest from pathlib import Path from unittest.mock import patch +import compare_results import hw_oracle @@ -41,16 +42,28 @@ class CompareResultsTests(unittest.TestCase): ], } - def cli(self, before=None, after=None): + def cli(self, before=None, after=None, process=False): self.before.write_text(json.dumps(self.report if before is None else before), encoding="utf-8") self.after.write_text(json.dumps(self.report if after is None else after), encoding="utf-8") - return subprocess.run([sys.executable, str(SCRIPT), str(self.before), str(self.after)], - capture_output=True, text=True, timeout=10) + if process: + return subprocess.run([sys.executable, str(SCRIPT), str(self.before), str(self.after)], + capture_output=True, text=True, timeout=10) + return self.compare() + + def compare(self): + argv = [str(SCRIPT), str(self.before), str(self.after)] + stdout, stderr = io.StringIO(), io.StringIO() + with patch("sys.argv", argv), contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr): + try: + returncode = compare_results.main() + except SystemExit as exit: + returncode = exit.code + return subprocess.CompletedProcess(argv, returncode, stdout.getvalue(), stderr.getvalue()) def test_identical_outputs_across_assembly_targets(self): after = copy.deepcopy(self.report) after["assembly_target"] = "gfx1100" - result = self.cli(after=after) + result = self.cli(after=after, process=True) self.assertEqual(result.returncode, 0, result.stderr) self.assertIn("gfx1030 -> gfx1100", result.stdout) self.assertIn("2 rows x 2 registers: 0 differing words", result.stdout) @@ -60,7 +73,7 @@ class CompareResultsTests(unittest.TestCase): after = copy.deepcopy(self.report) after["rows"][0]["output"][0] = 0x80000000 after["rows"][1]["output"] = [0x7fc00002, 0] - result = self.cli(after=after) + result = self.cli(after=after, process=True) self.assertEqual(result.returncode, 1, result.stderr) self.assertIn("3 differing words", result.stdout) self.assertIn("row 1, v10: 0x00000000 -> 0x80000000", result.stdout) @@ -147,8 +160,7 @@ class CompareResultsTests(unittest.TestCase): with self.subTest(content=content): if content is not None: self.after.write_bytes(content) - result = subprocess.run([sys.executable, str(SCRIPT), str(self.before), str(self.after)], - capture_output=True, text=True, timeout=10) + result = self.compare() self.assertEqual(result.returncode, 2) self.assertNotIn("Traceback", result.stderr) self.assertEqual(result.stdout, "") @@ -178,6 +190,26 @@ class CompareResultsTests(unittest.TestCase): self.assertIn("1 differing words", result.stdout) self.assertIn(f"row 1025, v{9 + outs}", result.stdout) + def test_diagnostic_limit_does_not_limit_comparison_or_hide_failure(self): + after = copy.deepcopy(self.report) + after["rows"][0]["output"][0] = 1 + after["rows"][1]["output"] = [2, 3] + self.cli(after=after) + for limit, shown in ((0, 0), (1, 1), (3, 3), (10, 3)): + with self.subTest(limit=limit): + result = subprocess.run([sys.executable, str(SCRIPT), str(self.before), str(self.after), + "--max-differences", str(limit)], capture_output=True, text=True) + self.assertEqual(result.returncode, 1, result.stderr) + self.assertIn("3 differing words", result.stdout) + self.assertEqual(sum(line.startswith("row ") for line in result.stdout.splitlines()), shown) + if shown < 3: + self.assertIn(f"{3 - shown} additional differing words omitted", result.stdout) + result = subprocess.run([sys.executable, str(SCRIPT), str(self.before), str(self.after), + "--max-differences", "-1"], capture_output=True, text=True) + self.assertEqual(result.returncode, 2) + self.assertEqual(result.stdout, "") + + if __name__ == "__main__": unittest.main() diff --git a/tools/hw-oracle/test_hw_oracle.py b/tools/hw-oracle/test_hw_oracle.py index 322e00a861..58f66cf38c 100644 --- a/tools/hw-oracle/test_hw_oracle.py +++ b/tools/hw-oracle/test_hw_oracle.py @@ -191,6 +191,40 @@ class InputRowsTests(unittest.TestCase): hw_oracle.main() self.assertEqual(run.call_args.args[1], [(1, 8, 0xffffffff, 2)]) + def test_validation_only_reports_dispatch_shape_without_running_tools(self): + with tempfile.TemporaryDirectory() as directory: + body, rows, extra = (Path(directory) / name for name in ("body.s", "rows.txt", "extra.bin")) + body.write_text("v_mov_b32 v10, v4", encoding="utf-8") + extra.write_bytes(b"abc") + options = [part for name, value in MODES.items() for part in ["--" + name.replace("_", "-"), str(value)]] + for count in (1, 33, 513, 1025): + rows.write_text("1 2 3 4\n" * count, encoding="utf-8") + for wave64 in (False, True): + argv = ["hw_oracle.py", str(body), str(rows), *options, "--validate-only", "--json", "--extra", str(extra)] + if wave64: + argv.append("--wave64") + with self.subTest(count=count, wave64=wave64), patch("sys.argv", argv), \ + patch.object(hw_oracle, "run", side_effect=AssertionError("GPU run")), \ + patch.object(hw_oracle, "target", side_effect=AssertionError("target lookup")), \ + contextlib.redirect_stdout(io.StringIO()) as output: + self.assertEqual(hw_oracle.main(), 0) + report = json.loads(output.getvalue()) + wave = 64 if wave64 else 32 + padded = (count + wave - 1) // wave * wave + dispatch = 512 if wave64 else 1024 + self.assertTrue(report["validation_only"]) + self.assertEqual((report["rows"], report["padded_rows"], report["dispatches"]), + (count, padded, (padded + dispatch - 1) // dispatch)) + self.assertEqual(report["extra_bytes"], 3) + self.assertEqual(report["floating_point_modes"], MODES) + self.assertNotIn("assembly_target", report) + for lds in (-1, 65537): + with self.subTest(lds=lds), patch("sys.argv", [*argv, "--lds", str(lds)]), \ + contextlib.redirect_stderr(io.StringIO()), self.assertRaises(SystemExit) as exited: + hw_oracle.main() + self.assertEqual(exited.exception.code, 2) + + class JsonReportTests(unittest.TestCase): def setUp(self): @@ -368,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 320db9d959..6ee58ad673 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -1,6 +1,7 @@ import argparse import hashlib import json +import os import struct import subprocess import sys @@ -13,7 +14,6 @@ import progress ROOT = Path(__file__).resolve().parent.parent PRX = ROOT / "core" / "libs" / "prx" -NID_POSTFIX = "_nid_postfix" MODULE_SUFFIX = "#" CLASSES = ("implemented", "stub", "absent", "module") PE_DIRECTORY_OFFSET = {0x20b: 112, 0x10b: 96} @@ -364,13 +364,14 @@ def built_libraries(directories): def built_host_dependencies(directories): - """Map each built .prx basename to the sorted .prx names it imports itself. + """Map each built .prx to its imported .prx names and identify PE importers. The loader resolves these host-to-host dependencies with LoadLibrary, so a name with no file in --libs or --modules fails even when every game import is implemented. System DLLs resolve through the OS and are not reported. """ direct = {} + windows = set() images = {} for directory in directories: for file in directory_files(directory, "*.prx"): @@ -383,18 +384,29 @@ def built_host_dependencies(directories): continue images[file.name] = (file, digest) direct[file.name] = sorted({name for name in read_host_imports(file, data) if name.lower().endswith(".prx")}) - return direct + if data[:2] == b"MZ": + windows.add(file.name) + return direct, windows -def audit_host_dependencies(direct, libraries, modules): +def audit_host_dependencies(direct, libraries, modules, windows=()): """Walk host imports transitively; list each missing library with its chain.""" available = set(libraries) | set(modules) + folded = defaultdict(set) + for name in available: + folded[name.lower()].add(name) missing = [] for root in sorted(direct): expanded = set() stack = [(name, [root, name]) for name in reversed(direct[root])] while stack: name, chain = stack.pop() + if chain[-2] in windows: + matches = folded.get(name.lower(), ()) + if len(matches) > 1: + raise AuditError(f"ambiguous Windows host library {name}: {', '.join(sorted(matches))}") + name = next(iter(matches), name) + chain[-1] = name if name not in available: missing.append({"importer": root, "chain": chain}) elif name in direct and name not in expanded and name not in chain[1:-1]: @@ -447,7 +459,7 @@ def read_modules(directories): def nids_of(name, aliases): if name in aliases: return aliases[name] - return {nid_names.compute_nid(name.removesuffix(NID_POSTFIX))} + return {nid_names.compute_nid(nid_names.strip_nid_postfix(name))} def source_stubs(source): @@ -522,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" @@ -545,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 @@ -653,7 +689,25 @@ def label(record): return f"{text} (imported by {record['module']})" if record.get("module") else text -def render(summary, records, missing, stops, missing_hosts=()): +def locate_missing(names, roots, depth=4): + wanted = {name.lower(): name for name in names} + found = {} + for root in roots: + root = Path(root) + if not root.is_dir(): + continue + base = len(root.parts) + for directory, directories, files in os.walk(root): + if len(Path(directory).parts) - base >= depth: + directories[:] = [] + for file in files: + name = wanted.get(file.lower()) + if name is not None and name not in found: + found[name] = Path(directory) + return found + + +def render(summary, records, missing, stops, missing_hosts=(), located=None): lines = [f"{summary['references']} references, {summary['unique_imports']} unique imports"] for kind in CLASSES: lines.append(f" {kind:<12}{summary['unique_by_class'][kind]:>6} imports {summary['references_by_class'][kind]:>6} references") @@ -680,6 +734,8 @@ def render(summary, records, missing, stops, missing_hosts=()): lines.extend(f" {library}: {count} imports" for library, count in missing.items()) if any(count == 0 for count in missing.values()): lines.append(" Libraries with 0 imports are declared dependencies; Windows can skip them if no import needs them") + for library, directory in sorted((located or {}).items()): + lines.append(f" {library} is in {directory}: pass that directory with --modules") if missing_hosts: lines.append("") lines.append(f"Host dependencies with no file in --libs or --modules ({len(missing_hosts)})") @@ -698,6 +754,8 @@ def main(argv=None): parser.add_argument("--source", default=str(PRX), help="core/libs/prx tree used to find throwing stubs (default: this repository's)") parser.add_argument("--names", help="aerolib.csv style file (NID, name per line) used to name absent imports") parser.add_argument("--json", type=Path, help="write the full result to this file") + parser.add_argument("--strict-stubs", action="store_true", + help="also fail when a referenced import resolves to a throwing stub") parser.add_argument("--strict-providers", action="store_true", help="fail when a named import is provided only by another library; retains the existing libc and title-module rules") args = parser.parse_args(argv) try: @@ -715,12 +773,13 @@ def main(argv=None): imports = [entry for _, entries in registries for entry in entries] needed = sorted({library for path in dependency_registries for library in read_needed_registry(path)}) exports, libraries = built_libraries(args.libs) - host_dependencies = built_host_dependencies(args.libs) + host_dependencies, windows = built_host_dependencies(args.libs) 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) + missing_hosts = audit_host_dependencies(host_dependencies, libraries, modules, windows) if args.names: attach_names(records, load_names(args.names)) summary = summarize(records, sum(record["references"] for record in records), missing, missing_hosts) @@ -739,8 +798,9 @@ def main(argv=None): except OSError as error: print(f"FAIL: {error.strerror}: {error.filename}" if error.filename else f"FAIL: {error}", file=sys.stderr) return 2 - print(render(summary, records, missing, stops, missing_hosts)) - return 1 if summary["unique_by_class"]["absent"] or missing or missing_hosts or (args.strict_providers and summary["library_mismatch"]) else 0 + located = locate_missing(missing, {path.resolve().parent for path in args.registries}) if missing else None + print(render(summary, records, missing, stops, missing_hosts, located)) + return 1 if summary["unique_by_class"]["absent"] or missing or missing_hosts or (args.strict_providers and summary["library_mismatch"]) or (args.strict_stubs and summary["unique_by_class"]["stub"]) else 0 if __name__ == "__main__": diff --git a/tools/nid_names.py b/tools/nid_names.py index 6ca6262fcb..1818f593c9 100644 --- a/tools/nid_names.py +++ b/tools/nid_names.py @@ -43,6 +43,10 @@ def compute_nid(symbol): return "".join(out) +def strip_nid_postfix(name): + return re.sub(r"_nid_disambig[0-9]+$", "", name.removesuffix(NID_POSTFIX)) + + def load_db(path): if not path.exists(): path.parent.mkdir(parents=True, exist_ok=True) @@ -126,12 +130,10 @@ def collect_real_names(): for match in source_definitions(text): name = match.group(1) names.add(name) - if name.endswith(NID_POSTFIX): - names.add(name[:-len(NID_POSTFIX)]) + names.add(strip_nid_postfix(name)) for name in source_assembly_definitions(text): names.add(name) - if name.endswith(NID_POSTFIX): - names.add(name[:-len(NID_POSTFIX)]) + names.add(strip_nid_postfix(name)) return names @@ -170,6 +172,14 @@ def nid_arguments(arguments, options): return normalized +def symbol_file(path): + if path == "-": + text = sys.stdin.buffer.read().decode("utf-8-sig") if hasattr(sys.stdin, "buffer") else sys.stdin.read().removeprefix("\ufeff") + else: + text = Path(path).read_text(encoding="utf-8-sig") + return [line for raw in text.split("\n") if (line := raw.removesuffix("\r"))] + + def main(argv=None): parser = argparse.ArgumentParser( description="Suggest real names for Unknown NID stubs") @@ -179,6 +189,8 @@ def main(argv=None): help="resolve only these NIDs instead of scanning the tree") parser.add_argument("--name", "--symbol", dest="name", action="append", default=None, help="compute NIDs for these symbol names") + parser.add_argument("--from-file", action="append", default=[], metavar="PATH", + help="append symbol names from a UTF-8 file, one nonempty line per name; repeat or use - for stdin") parser.add_argument("--json", action="store_true", help="emit machine-readable JSON") options = [option for action in parser._actions for option in action.option_strings] @@ -188,8 +200,14 @@ def main(argv=None): requested_nids = "--nid" in requested_options if requested_nids and requested_options & {"--name", "--symbol"}: parser.error("--name/--symbol cannot be combined with --nid") - if requested_options & {"--name", "--symbol"}: - rows = [{"name": name, "nid": compute_nid(name)} for name in (args.name or [])] + if requested_nids and "--from-file" in requested_options: + parser.error("--from-file cannot be combined with --nid") + if requested_options & {"--name", "--symbol", "--from-file"}: + try: + names = [*(args.name or []), *(name for path in args.from_file for name in symbol_file(path))] + except (OSError, UnicodeError) as error: + parser.error(str(error)) + rows = [{"name": name, "nid": compute_nid(name)} for name in names] if args.json: print(json.dumps({"symbols": rows}, indent=1)) else: diff --git a/tools/package_release.py b/tools/package_release.py index 25474b757e..88558908a6 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 [] @@ -46,13 +49,16 @@ def package(platform, build, output, version): raise RuntimeError(f"Missing patched libraries: {', '.join(sorted(missing))}") executable = "relinker.exe" if platform == "windows" else "relinker" files = [library for library in libraries if library.name in expected] + licenses = [] + if "libSceAjm.native.prx" in expected: + licenses.append((ROOT / "3rdparty/vgmstream/COPYING", "libs/licenses/vgmstream.txt")) if platform == "windows": runtime = windows_runtime(build) files.extend(runtime / name for name in ("libgcc_s_seh-1.dll", "libstdc++-6.dll", "libwinpthread-1.dll")) files.extend(build / "core/libs/libs" / name for name in ("mimalloc.dll", "mimalloc-redirect.dll")) files.extend(tracy_runtime(platform, build)) binary = build / "core/relinker" / executable - for file in [*files, binary]: + for file in [*files, binary, *(source for source, _ in licenses)]: if not file.is_file() or file.stat().st_size == 0: raise RuntimeError(f"Missing or empty release file: {file}") asset = f"relinker-{version}.exe" if platform == "windows" else f"relinker-{version}" @@ -73,9 +79,13 @@ def package(platform, build, output, version): with zipfile.ZipFile(staging / f"prx-{platform}-{version}.zip", "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive: for file in files: archive.write(file, arcname=f"libs/{file.name}") + for source, destination in licenses: + archive.write(source, arcname=destination) with tarfile.open(staging / f"prx-{platform}-{version}.tar.gz", "w:gz", compresslevel=9, dereference=True) as archive: for file in files: archive.add(file, arcname=f"libs/{file.name}") + for source, destination in licenses: + archive.add(source, arcname=destination) shutil.copy2(binary, staging / asset) for file in sorted(staging.iterdir()): file.replace(output / file.name) @@ -95,22 +105,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/pad_input.py b/tools/pad_input.py index a83c7a1116..ab93227072 100644 --- a/tools/pad_input.py +++ b/tools/pad_input.py @@ -26,13 +26,18 @@ def previous_sequence(path): if os.name == "nt" and line.endswith(b"\r\n"): line = line[:-2] + b"\n" tokens = line.split() - if len(line.rstrip(b"\n")) > 127 or len(tokens) not in (3, 7): + timeline = tokens[:1] == [b"APS5_PAD_TIMELINE"] + if timeline: + tokens = tokens[1:] + if len(line.rstrip(b"\n")) > 127 or len(tokens) not in ((3,) if timeline else (3, 7)): return 0 if any(re.fullmatch(rb"\+?[0-9]+", token) is None for token in tokens): return 0 values = [int(token) for token in tokens] if any(value > 2**64 - 1 for value in values): return 0 + if timeline: + return values[1] if values[0] == 1 and 1 <= values[2] <= 4096 else 0 if values[0] == 0 or values[1] & ~0x70fffe or not 1 <= values[2] <= 5000: return 0 if any(axis > 255 for axis in values[3:]): diff --git a/tools/pad_timeline.py b/tools/pad_timeline.py new file mode 100644 index 0000000000..16672d228e --- /dev/null +++ b/tools/pad_timeline.py @@ -0,0 +1,98 @@ +import argparse +import json +import os +import stat +from pathlib import Path +import tempfile + +from pad_input import BUTTONS, next_sequence + + +def integer(value, field, minimum, maximum): + if type(value) is not int or not minimum <= value <= maximum: + raise ValueError(f"{field} must be an integer between {minimum} and {maximum}") + return value + + +def compile_timeline(document, sequence, repeat=1, gap_ms=200): + integer(sequence, "sequence", 1, 2**64 - 1) + integer(repeat, "repeat", 1, 4096) + integer(gap_ms, "gap_ms", 0, 3600000) + if not isinstance(document, dict) or set(document) != {"version", "events"}: + raise ValueError("timeline must contain version and events") + if type(document["version"]) is not int or document["version"] != 1: + raise ValueError("unsupported timeline version") + source = document["events"] + if not isinstance(source, list) or not 1 <= len(source) * repeat <= 4096: + raise ValueError("timeline must contain 1 to 4096 events after repetition") + events = [] + previous_end = 0 + for index, event in enumerate(source): + prefix = f"events[{index}]" + if not isinstance(event, dict) or set(event) != {"at_ms", "hold_ms", "buttons"}: + raise ValueError(f"{prefix} must contain at_ms, hold_ms and buttons") + start = integer(event["at_ms"], f"{prefix}.at_ms", 0, 3600000) + duration = integer(event["hold_ms"], f"{prefix}.hold_ms", 1, 5000) + if start < previous_end or start + duration > 3600000: + raise ValueError(f"{prefix} overlaps an earlier event or exceeds one hour") + names = event["buttons"] + if not isinstance(names, list) or not all(isinstance(name, str) and name in BUTTONS for name in names): + raise ValueError(f"{prefix}.buttons must be a list of known button names") + if "release" in names and len(names) != 1: + raise ValueError(f"{prefix}: release cannot be combined with another button") + buttons = 0 + for name in names: + buttons |= BUTTONS[name] + events.append((start, duration, buttons)) + previous_end = start + duration + period = previous_end + gap_ms + if (repeat - 1) * period + previous_end > 3600000: + raise ValueError("repeated timeline exceeds one hour") + rows = [f"APS5_PAD_TIMELINE 1 {sequence} {len(events) * repeat}"] + for iteration in range(repeat): + rows.extend(f"{start + iteration * period} {duration} {buttons}" for start, duration, buttons in events) + return "\n".join(rows) + "\n" + + +def queue_timeline(path, text): + metadata = None + if os.name == "posix": + try: + metadata = path.stat() + except FileNotFoundError: + pass + temporary = None + try: + with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", newline="\n", dir=path.parent, delete=False) as output: + temporary = Path(output.name) + if metadata is not None: + if os.geteuid() == 0: + os.fchown(output.fileno(), metadata.st_uid, metadata.st_gid) + os.fchmod(output.fileno(), stat.S_IMODE(metadata.st_mode)) + output.write(text) + os.replace(temporary, path) + finally: + if temporary is not None: + temporary.unlink(missing_ok=True) + + +def main(): + parser = argparse.ArgumentParser(description="Queue a timed controller button sequence for APS5_PAD_INPUT_FILE.") + parser.add_argument("timeline", type=Path) + parser.add_argument("path", type=Path) + parser.add_argument("--repeat", type=int, default=1) + parser.add_argument("--gap-ms", type=int, default=200) + args = parser.parse_args() + try: + if args.timeline.resolve() == args.path.resolve() or (args.path.exists() and os.path.samefile(args.timeline, args.path)): + raise ValueError("the command file must be separate from the timeline JSON") + document = json.loads(args.timeline.read_text(encoding="utf-8")) + text = compile_timeline(document, next_sequence(args.path), args.repeat, args.gap_ms) + queue_timeline(args.path, text) + except (OSError, ValueError) as error: + parser.exit(2, f"pad_timeline: {error}\n") + print(f"Queued {len(text.splitlines()) - 1} timed events in {args.path}") + + +if __name__ == "__main__": + main() diff --git a/tools/performance_compare.py b/tools/performance_compare.py index bbbf7c949d..4642a983ff 100644 --- a/tools/performance_compare.py +++ b/tools/performance_compare.py @@ -6,6 +6,8 @@ from pathlib import Path CONTEXT = ("gpu", "driver", "os", "resolution") +OPTIONAL_STRINGS = ("cpu", "title_id", "title", "content_version") +OPTIONAL_INTEGERS = ("cpu_threads", "memory_mib") PERCENTILES = ("p50", "p90", "p99", "max") @@ -23,6 +25,12 @@ def validate(summary): for key in ("commit", "trace", *CONTEXT): if not isinstance(summary.get(key), str): raise ValueError(f"{key} must be a string") + for key in OPTIONAL_STRINGS: + if key in summary and not isinstance(summary[key], str): + raise ValueError(f"{key} must be a string") + for key in OPTIONAL_INTEGERS: + if key in summary and (type(summary[key]) is not int or summary[key] < 0): + raise ValueError(f"{key} must be a non-negative integer") if type(summary.get("trace_dropped")) is not bool: raise ValueError("trace_dropped must be a boolean") if "dirty" not in summary or (summary["dirty"] is not None and type(summary["dirty"]) is not bool): @@ -104,13 +112,14 @@ def compare(baseline, candidate): contexts = [] for summary in (baseline, candidate): context = {key: summary[key] for key in CONTEXT} + context.update({key: summary[key] for key in (*OPTIONAL_STRINGS, *OPTIONAL_INTEGERS) if key in summary}) context.update({f"window.{key}": summary["window"][key] for key in ("spec", "first_present")}) context.update({f"env.{key}": value for key, value in summary["env"].items()}) contexts.append(context) differences = {key: {"baseline": contexts[0].get(key), "candidate": contexts[1].get(key)} for key in sorted(contexts[0].keys() | contexts[1].keys()) if contexts[0].get(key) != contexts[1].get(key)} - notes = ["Compare captures from the same machine, title and scene; summaries do not record the CPU, title or scene.", + notes = ["Compare captures from the same machine, title, content version and scene; matching recorded metadata cannot verify the physical machine or scene.", "Changes are candidate minus baseline. A negative frame-time change means a shorter frame interval.", "A missing top phase is unreported, not zero; its change is unavailable."] if differences: @@ -120,7 +129,7 @@ def compare(baseline, candidate): notes.append(f"{label}: trace events were dropped; the event trace omits some of the work included in phase totals.") if summary["dirty"] is not False: notes.append(f"{label}: source tree is dirty or its state is unknown; the commit alone does not identify the build.") - missing = [key for key in CONTEXT if not summary[key]] + missing = [key for key in (*CONTEXT, *OPTIONAL_STRINGS, *OPTIONAL_INTEGERS) if not summary.get(key)] if missing: notes.append(f"{label}: missing configuration: {', '.join(missing)}.") return {"baseline": baseline, "candidate": candidate, "configuration_differences": differences, @@ -162,12 +171,16 @@ def main(argv=None): "Reports numerical differences, not statistical significance or an automatic pass/fail verdict.") parser.add_argument("baseline", type=Path, help="Baseline summary JSONL file") parser.add_argument("candidate", type=Path, help="Candidate summary JSONL file") + parser.add_argument("--require-matching-context", action="store_true", + help="reject differences in recorded configuration, including optional machine/title metadata; scene identity is not checked") parser.add_argument("--baseline-trace", help="Exact trace field to select in the baseline file") parser.add_argument("--candidate-trace", help="Exact trace field to select in the candidate file") parser.add_argument("--json", action="store_true", help="Write JSON instead of Markdown") args = parser.parse_args(argv) try: report = compare(load_summary(args.baseline, args.baseline_trace), load_summary(args.candidate, args.candidate_trace)) + if args.require_matching_context and report["configuration_differences"]: + raise ValueError("recorded configuration differs: " + ", ".join(report["configuration_differences"])) output = json.dumps(report, indent=2, ensure_ascii=False, allow_nan=False) + "\n" if args.json else markdown(report) except (OSError, ValueError, OverflowError) as error: parser.exit(2, f"{parser.prog}: {error}\n") 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 87d5010e9f..ca1bc134e5 100644 --- a/tools/pr_overlap.py +++ b/tools/pr_overlap.py @@ -10,6 +10,7 @@ from urllib.parse import quote, urlencode PARENT = 'if .parent then "\\(.parent.owner.login)/\\(.parent.name)" else .nameWithOwner end' EXPORT = re.compile(r"^\+.*\bAPS5_VABI\s+(\w+)\s*\(") +NID_DISAMBIG = re.compile(r"_nid_disambig[0-9]+$") DEPENDS = re.compile(r'Depends on:[ \t]*((?:#\d+\b(?:(?:[ \t]*,[ \t]*(?:and[ \t]+)?|[ \t]+and[ \t]+)#\d+\b)*|none\b)?(?:\r?\n[ \t]*[-*][ \t]*#\d+\b[^\r\n]*)*)') PLAIN_DEPENDS = re.compile(r'Depends on[ \t]+(#\d+\b(?:(?:[ \t]*,[ \t]*(?:and[ \t]+)?|[ \t]+and[ \t]+)#\d+\b)*)') HUNK = re.compile(r"^@@ -(\d+)(?:,(\d+))?", re.M) @@ -40,9 +41,7 @@ def upstream(): def depends_on(body): body = re.sub(r"<!--.*?-->", "", body or "", flags=re.S) - match = DEPENDS.search(body) - if not match or not match.group(1).strip(): - match = PLAIN_DEPENDS.search(body) + match = next((match for match in DEPENDS.finditer(body) if match.group(1).strip()), None) or PLAIN_DEPENDS.search(body) return {int(n) for n in re.findall(r"#(\d+)", match.group(1))} if match else set() @@ -112,7 +111,8 @@ def exports(diff): text += nxt[1:] semi, brace = text.find(";"), text.find("{") if semi < 0 or 0 <= brace < semi: - found.add(m.group(1)) + name = m.group(1).removesuffix("_nid_postfix") + found.add(NID_DISAMBIG.sub("", name)) return found @@ -241,6 +241,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") @@ -253,20 +259,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..944ba4170b 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: @@ -456,6 +456,31 @@ def write_site(output, libraries, shaders): (output / "progress.html").write_text(summary(libraries, shaders), encoding="utf-8") +def validate_snapshot(snapshot): + if not isinstance(snapshot, dict): + raise ValueError("snapshot must be an object") + for section in ("libraries", "shaders"): + data = snapshot.get(section) + if not isinstance(data, dict): + raise ValueError(f"{section} must be an object") + groups = data.get("groups") + if not isinstance(groups, list): + raise ValueError(f"{section}.groups must be a list") + percent = data.get("percent") + if isinstance(percent, bool) or not isinstance(percent, (int, float)) or not 0 <= percent <= 100: + raise ValueError(f"{section}.percent must be a finite number between 0 and 100") + for index, group in enumerate(groups): + location = f"{section}.groups[{index}]" + if not isinstance(group, dict) or not isinstance(group.get("name"), str): + raise ValueError(f"{location} must be an object with a string name") + if "shared_sources" in group and not isinstance(group["shared_sources"], str): + raise ValueError(f"{location}.shared_sources must be a string") + for field in ("done_names", "todo_names", "inert_names"): + values = group.get(field, []) + if not isinstance(values, list) or any(not isinstance(value, str) for value in values): + raise ValueError(f"{location}.{field} must be a list of strings") + + def main(argv=None): global PRX, OPCODES previous = PRX, OPCODES @@ -473,7 +498,18 @@ 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}") + for path, snapshot in zip(args.compare, snapshots): + try: + validate_snapshot(snapshot) + except 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): @@ -481,7 +517,10 @@ def main(argv=None): if args.root: PRX = args.root / "core" / "libs" / "prx" OPCODES = args.root / previous[1].relative_to(ROOT) - libraries, shaders = collect_libraries(), collect_shaders() + try: + libraries, shaders = collect_libraries(), collect_shaders() + except FileNotFoundError as error: + parser.error(f"{args.root or ROOT} is not a source tree: {error.filename} not found") try: libraries = select_libraries(libraries, args.module or []) except ValueError as error: diff --git a/tools/progress_history.py b/tools/progress_history.py index 5c5efe2f1b..ca8460652a 100644 --- a/tools/progress_history.py +++ b/tools/progress_history.py @@ -1,10 +1,14 @@ import argparse +import hashlib import io import json +import os +import stat +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 @@ -22,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()) @@ -52,12 +56,71 @@ def measure(sha): for name, data in (("libraries", libraries), ("shaders", shaders))} -def collect(ref): - points = [] +def cache_signature(): + digest = hashlib.sha256(repr(sys.version_info[:3]).encode("ascii")) + for path in (Path(__file__), Path(progress.__file__), Path(progress.tokenizer_spec.origin), progress.ISA): + content = path.read_bytes() + digest.update(len(content).to_bytes(8, "little")) + digest.update(content) + digest.update(repr((progress.PRX.relative_to(progress.ROOT).as_posix(), + progress.OPCODES.relative_to(progress.ROOT).as_posix())).encode("utf-8")) + return digest.hexdigest() + + +def load_cache(path, signature): + if not path.exists(): + return {} + data = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(data, dict) or type(data.get("schema_version")) is not int or data["schema_version"] != 1 or not isinstance(data.get("rules"), str) or not isinstance(data.get("commits"), dict): + raise ValueError(f"{path}: invalid history cache") + if data["rules"] != signature: + return {} + for sha, counts in data["commits"].items(): + if len(sha) != 40 or any(character not in "0123456789abcdef" for character in sha): + raise ValueError(f"{path}: invalid cached commit") + if counts is None: + continue + if not isinstance(counts, dict) or counts.keys() != {"libraries", "shaders"}: + raise ValueError(f"{path}: invalid cached counts for {sha}") + for metric in counts.values(): + if not isinstance(metric, dict) or metric.keys() != {"done", "total", "percent"}: + raise ValueError(f"{path}: invalid cached metric for {sha}") + done, total, percent = (metric[key] for key in ("done", "total", "percent")) + if type(done) is not int or type(total) is not int or not 0 <= done <= total or type(percent) not in (int, float) or percent != (round(100 * done / total, 2) if total else 0): + raise ValueError(f"{path}: inconsistent cached metric for {sha}") + return data["commits"] + + +def save_cache(path, signature, commits): + path.parent.mkdir(parents=True, exist_ok=True) + temporary = None + try: + with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", newline="\n", dir=path.parent, + prefix=".history-", delete=False) as output: + temporary = Path(output.name) + json.dump({"schema_version": 1, "rules": signature, "commits": commits}, output, sort_keys=True) + output.write("\n") + if path.exists(): + temporary.chmod(stat.S_IMODE(path.stat().st_mode)) + os.replace(temporary, path) + finally: + if temporary is not None: + temporary.unlink(missing_ok=True) + + +def collect(ref, cache=None): + signature = cache_signature() if cache is not None else None + records = load_cache(cache, signature) if cache is not None else {} + points, changed = [], False for day, sha in daily_commits(ref): - counts = measure(sha) + if sha not in records: + records[sha] = measure(sha) + changed = True + counts = records[sha] if counts: points.append({"date": day, "commit": sha, **counts}) + if cache is not None and changed: + save_cache(cache, signature, records) return points @@ -147,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>', @@ -162,8 +225,17 @@ if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("output", type=Path) parser.add_argument("--ref", default="HEAD", help="branch or commit whose first-parent history is measured") + parser.add_argument("--cache", type=Path, help="reuse per-commit counts while the scanner, ISA list and Python version match") args = parser.parse_args() - points = collect(args.ref) + try: + if args.cache: + outputs = [args.output / name for name in ("history.json", "history.svg", "history.html")] + if any(args.cache.resolve() == path.resolve() or + (args.cache.exists() and path.exists() and args.cache.samefile(path)) for path in outputs): + raise ValueError("history cache must differ from generated reports") + points = collect(args.ref, args.cache) + except (OSError, ValueError) as error: + parser.error(str(error)) if not points: parser.error("no commit with a measurable tree") args.output.mkdir(parents=True, exist_ok=True) diff --git a/tools/select_tests.py b/tools/select_tests.py index 5def841494..efb972d0a1 100644 --- a/tools/select_tests.py +++ b/tools/select_tests.py @@ -84,8 +84,8 @@ class Build: leaves.add(name) return sorted(leaves) - def tests(self): - listing = json.loads(subprocess.run(["ctest", "--test-dir", self.path, "--show-only=json-v1"], + def tests(self, *arguments): + listing = json.loads(subprocess.run(["ctest", "--test-dir", self.path, "--show-only=json-v1", *arguments], capture_output=True, text=True, check=True).stdout) return listing, listing["tests"] @@ -166,7 +166,7 @@ def dump(build, out): print(f"{len(tests)} tests, {len(closures)} test executables, {len(deps)} objects with header deps") -def select(graph, changed): +def select(graph, changed, reasons=None): full = [f for f in changed if FULL.search(f)] if full: return None, full @@ -180,15 +180,28 @@ def select(graph, changed): if (test["target"] is None or files & set(test["registered"]) or files & scope or any(f.startswith(dirs) for f in scope | set(test["registered"]))): picked.add(name) + if reasons is not None: + reasons[name] = (["no build executable"] if test["target"] is None else []) + reasons[name] += ["input: " + path for path in sorted(files & scope)] + reasons[name] += ["registration: " + path for path in sorted(files & set(test["registered"]))] + reasons[name] += ["directory: " + directory for directory in dirs + if any(path.startswith(directory) for path in scope | set(test["registered"]))] setups = collections.defaultdict(set) for name, test in tests.items(): for fixture in test["sets"]: setups[fixture].add(name) todo = list(picked) while todo: - test = tests[todo.pop()] + name = todo.pop() + test = tests[name] for extra in set(test["depends"]).union(*(setups[f] for f in test["requires"])): - if extra in tests and extra not in picked: + if extra not in tests: + continue + if reasons is not None: + fixture = set(test["requires"]) & set(tests[extra]["sets"]) + kind = "fixture setup for: " if fixture else "dependency of: " + reasons.setdefault(extra, []).append(kind + name) + if extra not in picked: picked.add(extra) todo.append(extra) return picked, [] @@ -197,7 +210,12 @@ 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"]] - names = {listed[i - 1]["name"] for i in run} + 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] keep = {build.output(a) for t in listed if t["name"] in names for a in t.get("command") or [] if os.path.isabs(a)} skip = {build.target(t) for t in listed if t["name"] not in names} - keep - {None} return run, [t for t in build.leaves() if t not in skip], len(listed) @@ -217,6 +235,9 @@ def main(): s.add_argument("build") s.add_argument("graph") s.add_argument("changed", help="file with one changed path per line") + e = sub.add_parser("explain", help="explain saved-graph selections without a configured build; CTest may add fixtures") + e.add_argument("graph", type=Path) + e.add_argument("changed", type=Path, help="file with one changed path per line") c = sub.add_parser("ctest", help="run ctest on a tree, limited to the selected tests if select wrote any") c.add_argument("build") c.add_argument("arguments", nargs=argparse.REMAINDER, help="ctest arguments") @@ -225,7 +246,22 @@ def main(): selected = Path(args.build, "select.tests") extra = ["-I", selected.read_text(encoding="utf-8").strip()] if selected.exists() else [] return subprocess.run(["ctest", "--test-dir", args.build, *args.arguments, *extra]).returncode - build = Build(args.build) + if args.command == "explain": + try: + graph = json.loads(args.graph.read_text(encoding="utf-8")) + changed = [line.strip() for line in args.changed.read_text(encoding="utf-8").splitlines() if line.strip()] + reasons = {} + picked, full = select(graph, changed, reasons) + except (OSError, ValueError) as error: + parser.error(str(error)) + print(json.dumps({"tests": sorted(picked) if picked is not None else None, + "full_run_paths": full, "reasons": {name: sorted(set(why)) for name, why in reasons.items()}}, + indent=2, sort_keys=True)) + return 0 + try: + build = Build(args.build) + except (OSError, KeyError) as error: + parser.error(f"{args.build}: not a configured CMake build directory ({error})") if args.command == "dump": dump(build, args.graph) return 0 @@ -235,7 +271,7 @@ def main(): print("full run: no test graph") return 0 graph = json.loads(Path(args.graph).read_text(encoding="utf-8")) - changed = [line.strip() for line in Path(args.changed).read_text(encoding="utf-8").splitlines() if line.strip()] + changed = [line.strip() for line in Path(args.changed).read_text(encoding="utf-8-sig").splitlines() if line.strip()] picked, full = select(graph, changed) if picked is None: print(f"full run: {', '.join(full[:5])}") 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 3b83764f0a..3dcc4b1d43 100644 --- a/tools/tests/test_check_compile.py +++ b/tools/tests/test_check_compile.py @@ -1,11 +1,13 @@ import json import os +import shlex import shutil import subprocess 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 +16,69 @@ TOOL = Path(__file__).resolve().parents[1] / "check_compile.py" class CheckCompileTests(unittest.TestCase): + def test_unexpanded_response_files_cannot_rewrite_build_outputs(self): + compiler = shutil.which("clang") + if compiler is None: + self.skipTest("Clang is required to check compilation-fragment side effects") + source = self.source("unit.c") + outputs = [self.build / name for name in ("unit.o", "unit.d", "fragment.json")] + response = self.build / "unit flags.rsp" + response.write_text(" ".join('"' + argument + '"' for argument in + ("-c", source.as_posix(), "-MMD", "-MF", outputs[1].as_posix(), + "-MJ", outputs[2].as_posix(), "-o", outputs[0].as_posix())), encoding="utf-8") + arguments = [compiler, "@" + str(response)] + original = subprocess.run(arguments, cwd=self.build, capture_output=True, text=True) + self.assertEqual(original.returncode, 0, original.stderr) + preserved = {path: path.read_bytes() for path in [source, response, *outputs]} + for field in ("arguments", "command"): + for jobs in ("1", "2"): + with self.subTest(field=field, jobs=jobs): + command = (subprocess.list2cmdline(arguments) if os.name == "nt" else + " ".join(shlex.quote(argument) for argument in arguments)) + self.manifest([{"directory": str(self.build), "file": str(source), + field: arguments if field == "arguments" else command}]) + result = self.tool("--jobs", jobs, "unit.c") + self.assertEqual(result.returncode, 2, result.stdout + result.stderr) + self.assertIn("unexpanded response file", result.stderr) + self.assertIn(str(response), result.stderr) + self.assertNotIn("Traceback", result.stderr) + for path, content in preserved.items(): + self.assertEqual(path.read_bytes(), content) + + 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 +97,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: @@ -243,6 +345,37 @@ class CheckCompileTests(unittest.TestCase): self.assertEqual(result.returncode, 0, result.stderr) self.assertIn("1 translation units compile", result.stdout) + def test_a_generated_translation_unit_outside_the_checkout_is_checked(self): + with tempfile.TemporaryDirectory() as directory: + source = Path(directory).resolve() / "generated unit.cpp" + source.write_text("int x;\n", encoding="utf-8") + seen = self.root / "arguments.json" + script = self.compiler( + "import json\nimport sys\nfrom pathlib import Path\n" + "Path(sys.argv[1]).write_text(json.dumps(sys.argv[3:]))\n" + "if sys.argv[2] == '1': sys.stderr.write('external source: error: no\\n')\n" + "sys.exit(int(sys.argv[2]))\n") + for relative in (False, True): + for status in (0, 1): + with self.subTest(relative=relative, status=status): + self.manifest([{ + "directory": str(self.build), + "file": os.path.relpath(source, self.build) if relative else str(source), + "arguments": [sys.executable, str(script), str(seen), str(status), + "-c", "-o", "generated.o", str(source)], + }]) + path = os.path.relpath(source, self.root) if relative else str(source) + result = self.tool(path) + self.assertNotIn("no compile command", result.stderr) + self.assertEqual(result.returncode, status, result.stdout + result.stderr) + self.assertEqual(json.loads(seen.read_text(encoding="utf-8")), + ["-fsyntax-only", str(source)]) + if status: + self.assertIn("external source: error: no", result.stderr) + self.assertIn("1 of 1 translation units did not compile", result.stderr) + else: + self.assertIn("1 translation units compile", result.stdout) + def test_a_changed_file_with_a_non_ascii_name_is_checked(self): self.failing("caf\u00e9.cpp") subprocess.run(["git", "-C", str(self.root), "add", "caf\u00e9.cpp"], check=True) @@ -327,14 +460,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) @@ -370,6 +506,39 @@ class CheckCompileTests(unittest.TestCase): self.assertIn("arguments must be a nonempty list of strings with an executable", result.stderr) self.assertNotIn("Traceback", result.stderr) + def test_parallel_checks_overlap_but_diagnostics_keep_source_order(self): + barrier = self.root / "barrier" + barrier.mkdir() + compiler = self.compiler( + "import sys, time\nfrom pathlib import Path\n" + "root, name = Path(sys.argv[1]), sys.argv[2]\n" + "(root / name).touch()\n" + "deadline = time.monotonic() + 5\n" + "while not all((root / value).exists() for value in ('first', 'second')):\n" + " if time.monotonic() > deadline: sys.exit('checks did not overlap')\n" + " time.sleep(0.01)\n" + "if name == 'second': (root / 'second.done').touch()\n" + "while name == 'first' and not (root / 'second.done').exists():\n" + " if time.monotonic() > deadline: sys.exit('second did not finish')\n" + " time.sleep(0.01)\n" + "sys.exit(name + ': diagnostic')\n") + entries = [] + for name in ("first", "second"): + source = self.source(name + ".c") + entries.append(self.entry(name + ".c", f'"{sys.executable}" "{compiler}" "{barrier}" {name} "{source}"')) + self.manifest(entries) + result = self.tool("--jobs", "2", "first.c", "second.c") + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("2 of 2 translation units did not compile", result.stderr) + self.assertLess(result.stderr.index("first: diagnostic"), result.stderr.index("second: diagnostic")) + self.assertNotIn("did not overlap", result.stderr) + for jobs in ("0", "-1"): + with self.subTest(jobs=jobs): + result = self.tool("--jobs", jobs, "first.c") + self.assertEqual(result.returncode, 2) + self.assertIn("positive integer", result.stderr) + + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_check_conventions_blob_reads.py b/tools/tests/test_check_conventions_blob_reads.py index b277451f3b..17b5795272 100644 --- a/tools/tests/test_check_conventions_blob_reads.py +++ b/tools/tests/test_check_conventions_blob_reads.py @@ -21,7 +21,7 @@ EXPECTED = [ ] -def repository(root, fillers): +def repository(root, fillers, exports=0): def git(*args): subprocess.run(["git", "-c", f"safe.directory={root}", "-c", "user.name=Conventions Test", "-c", "user.email=test@example.com", "-c", "commit.gpgsign=false", *args], @@ -39,12 +39,18 @@ def repository(root, fillers): write("docs/dev/debt/libSceExample.md", f"# libSceExample technical debt\n\n### Silent stubs\n\n- [Known](../../../{EXPORT}) - layout unknown.\n") write("core/libs/prx/libSceOther/src/Other.cpp", "int APS5_VABI sceShared() { return now(); }\n") + if exports: + names = [f"sceMany{index:04d}" + "LongExportName" * 6 for index in range(exports)] + write("core/libs/prx/libSceOther/src/Many.cpp", + f"int APS5_VABI {names[-1]}Extra() {{ return now(); }}\nint APS5_VABI {names[-1]}() {{ return now(); }}\n") write("removed.txt", "removed\n") git("add", ".") git("commit", "-qm", "docs: add base") git("tag", "base") write(EXPORT, "int APS5_VABI sceExample() { return 0; }\nint APS5_VABI sceShared() { return now(); }\nint value; // note\n") write(UNLISTED, "int APS5_VABI sceUnlisted() { return 0; }\n") + if exports: + write("core/libs/prx/libSceMany/Export.cpp", "".join(f"int APS5_VABI {name}() {{ return now(); }}\n" for name in names)) write("data.txt", b"\xff\xfe not utf-8\n") write("docs/user/Café.md", "# Café\n") (root / "removed.txt").unlink() @@ -110,6 +116,16 @@ class BlobReadTests(unittest.TestCase): self.assertEqual(many_findings, few_findings) self.assertEqual(many, few) + def test_many_exports_keep_duplicate_detection(self): + root = Path(self.directory.name) / "many-exports" + root.mkdir() + repository(root, 2, exports=400) + findings, _ = self.run_checks(root) + self.assertEqual(findings, sorted(EXPECTED + [ + ("duplicate-export", "core/libs/prx/libSceMany/Export.cpp", 400, + "sceMany0399" + "LongExportName" * 6 + " is also in libSceOther") + ])) + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_check_conventions_doc_links.py b/tools/tests/test_check_conventions_doc_links.py index 3f847eae90..ec02294932 100644 --- a/tools/tests/test_check_conventions_doc_links.py +++ b/tools/tests/test_check_conventions_doc_links.py @@ -21,6 +21,9 @@ class DocLinkTests(unittest.TestCase): text = f"{FENCE}\r\ncode\r\n{FENCE}\r\nsee [guide](../README.md)\r\n" self.assertEqual(check_conventions.links("docs/x.md", text), [("../README.md", "README.md")]) + def test_link_text_in_inline_code_is_kept(self): + self.assertEqual(check_conventions.links("docs/x.md", "see [`Driver::Run`](../README.md)"), [("../README.md", "README.md")]) + def test_empty_link_text_is_ignored(self): self.assertEqual(check_conventions.links("docs/x.md", "![](img.png) and [](empty.md)"), []) 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_check_skips.py b/tools/tests/test_check_skips.py index f56f9cc50a..f4de797f05 100644 --- a/tools/tests/test_check_skips.py +++ b/tools/tests/test_check_skips.py @@ -152,6 +152,29 @@ class CheckSkipsTests(unittest.TestCase): self.assertEqual(result.returncode, 0, result.stdout + result.stderr) self.assertIn("all expected", result.stdout) + def test_no_usable_vulkan_device_is_expected_in_both_jobs(self): + reason = "no usable Vulkan device: ErrorInitializationFailed" + with tempfile.TemporaryDirectory() as directory: + junit = Path(directory) / "junit.xml" + junit.write_text(f'<testsuite><testcase name="shader_f64_fma"><skipped/><system-out>skipped, {reason}</system-out></testcase></testsuite>', encoding="utf-8") + for job in ("linux", "windows"): + with self.subTest(job=job): + result = subprocess.run( + [sys.executable, "-I", str(TOOL), str(junit), "--job", job], + capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("all expected", result.stdout) + + def test_device_feature_limitation_stays_unexpected_on_windows(self): + with tempfile.TemporaryDirectory() as directory: + junit = Path(directory) / "junit.xml" + junit.write_text('<testsuite><testcase name="shader_subgroup"><skipped/><system-out>skipped, the device lacks shaderSubgroupClock</system-out></testcase></testsuite>', encoding="utf-8") + result = subprocess.run( + [sys.executable, "-I", str(TOOL), str(junit), "--job", "windows"], + capture_output=True, text=True) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("error: shader_subgroup skipped", result.stdout) + def test_list_reports_skipped_reasons_including_indented_lines(self): xml = ( '<testsuite>' diff --git a/tools/tests/test_configure_seed.py b/tools/tests/test_configure_seed.py index e1a1413fca..2cff72cd94 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,103 @@ 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_unreadable_cache_is_reported_without_a_traceback(self): + missing = self.root / "no cache dir" / "CMakeCache.txt" + result = subprocess.run([sys.executable, str(TOOL), str(missing), str(self.root / "seed.cmake")], + capture_output=True, text=True) + self.assertEqual(result.returncode, 2, result.stderr) + self.assertIn(str(missing), result.stderr) + self.assertNotIn("Traceback", result.stderr) + self.assertFalse((self.root / "seed.cmake").exists()) + + def test_missing_seed_directory_is_created(self): + seed = self.root / "new dir" / "nested" / "seed.cmake" + result = self.run_seed(seed) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("HAVE_FEATURE", seed.read_text(encoding="utf-8")) + + 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,6 +145,53 @@ class ConfigureSeedTests(unittest.TestCase): subprocess.run([sys.executable, str(TOOL), str(cache), str(seed)], check=True, capture_output=True) return seed + +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_cache_without_checks_writes_an_empty_seed(self): + cache = self.root / "empty" / "CMakeCache.txt" + cache.parent.mkdir() + cache.write_text("//Path to a program.\nCMAKE_AR:FILEPATH=/usr/bin/ar\n", encoding="utf-8") + self.assertEqual(self.seed(cache, "empty.cmake").read_text(encoding="utf-8"), "") + + def test_seed_filters_leave_excluded_checks_to_the_new_configure(self): + seed = self.root / "filtered.cmake" + result = subprocess.run([sys.executable, str(TOOL), str(self.root / "fresh" / "CMakeCache.txt"), str(seed), + "--include", "MINI_HAVE_*", "--include", "MINI_COMPILES", "--exclude", "*MISSING*"], + check=True, capture_output=True, text=True) + self.assertIn("3 check results", result.stdout) + seeded = seed.read_text(encoding="utf-8") + self.assertIn("MINI_HAVE_STDIO_H", seeded) + self.assertIn("MINI_HAVE_PRINTF", seeded) + self.assertIn("MINI_COMPILES", seeded) + self.assertNotIn("MINI_HAVE_MISSING_H", seeded) + self.assertNotIn("MINI_FAILS", seeded) + output = self.configure("filter-seeded", "-C", str(seed)) + self.assertIn("Looking for mini_missing_header.h", output) + self.assertNotIn("Looking for stdio.h", output) + self.assertNotIn("Performing Test MINI_COMPILES", output) + self.assertEqual(configure_seed.checks(self.root / "filter-seeded" / "CMakeCache.txt"), + configure_seed.checks(self.root / "fresh" / "CMakeCache.txt")) + subprocess.run([sys.executable, str(TOOL), str(self.root / "fresh" / "CMakeCache.txt"), str(seed), + "--include", "no_match_*"], check=True, capture_output=True) + 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"))) @@ -67,12 +201,6 @@ class ConfigureSeedTests(unittest.TestCase): self.assertEqual(fresh["MINI_HAVE_STDIO_H"][0], "1") self.assertEqual(fresh["MINI_FAILS"][0], "") - 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() @@ -83,11 +211,15 @@ class ConfigureSeedTests(unittest.TestCase): 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() - cache.write_text("//Path to a program.\nCMAKE_AR:FILEPATH=/usr/bin/ar\n", encoding="utf-8") - self.assertEqual(self.seed(cache, "empty.cmake").read_text(encoding="utf-8"), "") + + +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__": diff --git a/tools/tests/test_debt_split.py b/tools/tests/test_debt_split.py index 14eb2bac77..1f2cf31775 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" @@ -198,6 +201,16 @@ class DebtSplitTests(unittest.TestCase): "\n\n### Functional\n\n" + A_FUNCTIONAL.replace("../../", "../../../") + "\n") self.assertEqual(self.read(INDEX), self.git("show", "main:" + INDEX).stdout) + def test_resolve_places_an_entry_inserted_at_the_top_of_a_later_section(self): + added = "- [libSceA](../../core/libs/prx/libSceA/Added.cpp) - `sceAAdded` returns 0" + result = self.rebase([A_STUB, B_STUB, C_STUB], [added, A_FUNCTIONAL, SHADER, SHARED]) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.git("rebase", "--continue") + self.assertEqual(self.read("docs/dev/debt/libSceA.md"), + "# libSceA technical debt\n\n### Silent stubs\n\n" + A_STUB.replace("../../", "../../../") + + "\n\n### Functional\n\n" + + "\n".join(entry.replace("../../", "../../../") for entry in (added, A_FUNCTIONAL)) + "\n") + def test_indented_continuation_after_a_blank_line_moves_with_its_entry(self): note = " - sub note about sceAOne" self.write(INDEX, "# Project technical debt\n\n### Silent stubs\n\n" + A_STUB + "\n\n" + note + "\n") @@ -212,6 +225,22 @@ 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")) + + def test_split_keeps_blank_line_between_staying_entry_and_paragraph(self): + paragraph = "A paragraph that belongs to no library." + self.write(INDEX, "# Project technical debt\n\n### Silent stubs\n\n" + A_STUB + + "\n- A toolchain limitation\n\n" + paragraph + "\n") + self.split() + self.assertIn("- A toolchain limitation\n\n" + paragraph + "\n", self.read(INDEX)) + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_env_reference.py b/tools/tests/test_env_reference.py index ae390c5e9d..c414e314c0 100644 --- a/tools/tests/test_env_reference.py +++ b/tools/tests/test_env_reference.py @@ -1,3 +1,4 @@ +import contextlib import io import sys import tempfile @@ -25,6 +26,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 +51,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 +62,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() @@ -101,6 +110,26 @@ class EnvReferenceTests(unittest.TestCase): env_reference.check(found, root / "missing.md", env_reference.PREFIXES, out), 1) self.assertIn("cannot read", out.getvalue()) + def test_ancestor_directory_named_in_skip_does_not_suppress_root_scan(self): + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + core = Path(directory.name) / "build" / "checkout" / "core" + (core / "3rdparty").mkdir(parents=True) + (core / "Sample.cpp").write_text('static bool a = IsEnvironmentSet("APS5_TRACE_GPU");\n', encoding="utf-8") + (core / "3rdparty" / "Ignored.cpp").write_text('static bool b = IsEnvironmentSet("APS5_IGNORED");\n', encoding="utf-8") + self.assertEqual(set(env_reference.reads([core], BOTH)), {"APS5_TRACE_GPU"}) + + def test_main_rejects_a_scan_root_that_is_not_a_directory(self): + root = self.fixture() + for name in ("missing", "core/Sample.cpp"): + for flags in (["--check"], ["--table"], []): + err = io.StringIO() + with contextlib.redirect_stdout(io.StringIO()) as out, contextlib.redirect_stderr(err): + code = env_reference.main(flags + ["--doc", str(root / "ENVIRONMENT.md"), "--root", str(root / name)]) + self.assertEqual(code, 1, (name, flags)) + self.assertIn("is not a directory", err.getvalue()) + self.assertNotIn("no longer reads", out.getvalue()) + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_import_audit.py b/tools/tests/test_import_audit.py index 39c0e5e614..a29775c13b 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()) @@ -662,6 +682,22 @@ class ImportAuditTests(unittest.TestCase): self.assertEqual(data["missing_libraries"], {"libSceMissing.prx": 1}) self.assertIn("libSceMissing.prx", out) + def test_missing_library_is_located_next_to_the_registry(self): + work = self.work + (work.libs / "libc.prx").write_bytes(exporting()) + (work.modules / "main.prx").write_bytes(build_elf([], ["libSceMissing.prx", "libSceGone.prx"])) + registry = work.root / "game.json" + write_registry(registry, []) + elsewhere = work.root / "in" / "Media Modules" + elsewhere.mkdir(parents=True) + (elsewhere / "libSceMissing.prx").write_bytes(exporting("DDDDDDDDDDD")) + code, out, err = work.run(registry) + self.assertEqual((code, err), (1, "")) + self.assertIn(f"libSceMissing.prx is in {elsewhere.resolve()}: pass that directory with --modules", out) + self.assertNotIn("libSceGone.prx is in", out) + code, out, err = work.run(registry, "--modules", str(elsewhere)) + self.assertNotIn("libSceMissing.prx is in", out) + def test_conflicting_module_files_preserve_or_leave_no_json_report(self): work = self.work first, second = work.modules, work.root / "other-modules" @@ -890,6 +926,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")) @@ -1047,6 +1127,19 @@ class ImportAuditTests(unittest.TestCase): self.assertNotIn(("libSceTwin.native.prx", twin), stubs) self.assertNotEqual(nid_names.compute_nid("printf"), nid_names.compute_nid("printf_nid_postfix")) + def test_disambiguated_stubs_use_the_nid_of_their_symbol(self): + source = self.work.source + write_source(source, "libkernel", STUB_SOURCE.format(name="fstat_nid_disambig1_nid_postfix") + + STUB_SOURCE.format(name="pwrite_nid_disambig12") + STUB_SOURCE.format(name="sceKeep_nid_disambigA")) + stubs = import_audit.source_stubs(source) + fstat = nid_names.compute_nid("fstat") + self.assertEqual(stubs, { + ("libkernel.prx", fstat): "fstat_nid_disambig1_nid_postfix", + ("libkernel.prx", nid_names.compute_nid("pwrite")): "pwrite_nid_disambig12", + ("libkernel.prx", nid_names.compute_nid("sceKeep_nid_disambigA")): "sceKeep_nid_disambigA"}) + records, _ = import_audit.audit([(fstat, "libkernel.prx", False)], {fstat: ["libkernel.prx"]}, stubs, {"libkernel.prx"}, set()) + self.assertEqual((records[0]["class"], records[0]["name"]), ("stub", "fstat_nid_disambig1_nid_postfix")) + def test_stub_in_one_library_is_not_hidden_by_another(self): source = self.work.source write_source(source, "libA", DONE_SOURCE.format(name="sceTwinCall")) @@ -1458,6 +1551,88 @@ class ImportAuditTests(unittest.TestCase): self.assertEqual((code, err), (0, "")) self.assertNotIn("Host dependencies", out) + def test_pe_host_dependencies_ignore_library_name_case(self): + work = self.work + (work.libs / "libSceB.prx").write_bytes(build_pe_with_imports([], ["BBBBBBBBBBB"])) + registry, report = work.root / "game.json", work.root / "audit.json" + write_registry(registry, []) + for dependency in ("LIBSCEB.PRX", "libsceb.prx", "LiBsCeB.PrX"): + with self.subTest(dependency=dependency): + (work.libs / "libSceA.prx").write_bytes(build_pe_with_imports([dependency], ["AAAAAAAAAAA"])) + code, out, err = work.run(registry, "--json", str(report)) + self.assertEqual((code, err), (0, ""), out) + self.assertEqual(json.loads(report.read_text())["missing_host_dependencies"], []) + + def test_pe_host_dependency_case_does_not_hide_a_transitive_gap(self): + work = self.work + (work.libs / "libSceA.prx").write_bytes(build_pe_with_imports(["LIBSCEB.PRX"])) + (work.libs / "libSceB.prx").write_bytes(build_pe_with_imports(["LIBSCEMISSING.PRX"])) + registry, report = work.root / "game.json", work.root / "audit.json" + write_registry(registry, []) + code, _, err = work.run(registry, "--json", str(report)) + self.assertEqual((code, err), (1, "")) + self.assertEqual(json.loads(report.read_text())["missing_host_dependencies"], [ + {"importer": "libSceA.prx", "chain": ["libSceA.prx", "libSceB.prx", "LIBSCEMISSING.PRX"]}, + {"importer": "libSceB.prx", "chain": ["libSceB.prx", "LIBSCEMISSING.PRX"]}, + ]) + + def test_pe_host_dependency_can_find_a_module_with_different_case(self): + work = self.work + (work.libs / "libSceA.prx").write_bytes(build_pe_with_imports(["LIBSCEIME.PRX"])) + (work.modules / "libSceIme.prx").write_bytes(exporting()) + registry = work.root / "game.json" + write_registry(registry, []) + code, out, err = work.run(registry) + self.assertEqual((code, err), (0, ""), out) + + def test_pe_host_dependency_can_find_a_converted_pe_module_with_different_case(self): + work = self.work + (work.libs / "libSceA.prx").write_bytes(build_pe_with_imports(["LIBSCEIME.PRX"])) + (work.modules / "libSceIme.prx.guest.prx").write_bytes(build_pe_with_imports([])) + registry, report = work.root / "game.json", work.root / "audit.json" + write_registry(registry, []) + code, out, err = work.run(registry, "--json", str(report)) + self.assertEqual((code, err), (0, ""), out) + self.assertEqual(json.loads(report.read_text())["missing_host_dependencies"], []) + + def test_elf_host_dependency_case_stays_significant_in_a_mixed_graph(self): + work = self.work + (work.libs / "libSceA.prx").write_bytes(build_pe_with_imports(["LIBSCEB.PRX"])) + (work.libs / "libSceB.prx").write_bytes(build_elf([], ["LIBSCEC.PRX"])) + (work.libs / "libSceC.prx").write_bytes(exporting()) + registry, report = work.root / "game.json", work.root / "audit.json" + write_registry(registry, []) + code, _, err = work.run(registry, "--json", str(report)) + self.assertEqual((code, err), (1, "")) + self.assertEqual(json.loads(report.read_text())["missing_host_dependencies"], [ + {"importer": "libSceA.prx", "chain": ["libSceA.prx", "libSceB.prx", "LIBSCEC.PRX"]}, + {"importer": "libSceB.prx", "chain": ["libSceB.prx", "LIBSCEC.PRX"]}, + ]) + + def test_pe_host_dependency_rejects_ambiguous_case_variants(self): + work = self.work + other = work.root / "other-libs" + other.mkdir() + (work.libs / "libSceA.prx").write_bytes(build_pe_with_imports(["LIBSCEB.PRX"])) + (work.libs / "libSceB.prx").write_bytes(build_pe_with_imports([], ["AAAAAAAAAAA"])) + (other / "LIBSCEB.prx").write_bytes(build_pe_with_imports([], ["BBBBBBBBBBB"])) + registry, report = work.root / "game.json", work.root / "audit.json" + write_registry(registry, []) + report.write_bytes(b"previous complete report") + code, out, err = work.run(registry, "--libs", str(other), "--json", str(report)) + self.assertEqual((code, out), (2, "")) + self.assertIn("ambiguous Windows host library", err) + self.assertEqual(report.read_bytes(), b"previous complete report") + + def test_pe_host_dependency_cycle_with_different_case_terminates(self): + work = self.work + (work.libs / "libSceA.prx").write_bytes(build_pe_with_imports(["LIBSCEB.PRX"])) + (work.libs / "libSceB.prx").write_bytes(build_pe_with_imports(["LIBSCEA.PRX"])) + registry = work.root / "game.json" + write_registry(registry, []) + code, out, err = work.run(registry) + self.assertEqual((code, err), (0, ""), out) + def test_elf_host_dependencies_are_walked(self): work = self.work (work.libs / "libSceA.prx").write_bytes(build_elf( @@ -1692,6 +1867,26 @@ class ImportAuditTests(unittest.TestCase): self.assertEqual((code, out), (2, "")) self.assertIn(f"FAIL: Permission denied: {library}", err) + def test_strict_stubs_fails_only_for_referenced_throwing_providers(self): + name = "sceExamplePending" + nid = nid_names.compute_nid(name) + registry = self.work.root / "game.json" + output = self.work.root / "audit.json" + (self.work.libs / "libSceExample.prx").write_bytes(exporting(nid)) + write_source(self.work.source, "libSceExample", STUB_SOURCE.format(name=name)) + for weak in (False, True): + with self.subTest(weak=weak): + write_registry(registry, [(nid, "libSceExample.prx", weak)]) + self.assertEqual(self.work.run(registry)[0], 0) + code, out, err = self.work.run(registry, "--strict-stubs", "--json", str(output)) + self.assertEqual((code, err), (1, "")) + self.assertIn("stub", out) + self.assertEqual(json.loads(output.read_text(encoding="utf-8"))["unique_by_class"]["stub"], 1) + write_source(self.work.source, "libSceExample", DONE_SOURCE.format(name=name) + + STUB_SOURCE.format(name="sceUnreferencedPending")) + self.assertEqual(self.work.run(registry, "--strict-stubs")[0], 0) + + class StrictProviderTests(unittest.TestCase): def test_named_provider_mismatch_is_optional_failure(self): diff --git a/tools/tests/test_nid_names.py b/tools/tests/test_nid_names.py index 5054e58516..8445352ecd 100644 --- a/tools/tests/test_nid_names.py +++ b/tools/tests/test_nid_names.py @@ -210,6 +210,29 @@ class NidArgumentTests(unittest.TestCase): rows = json.loads(printed.call_args.args[0])["rows"] self.assertEqual({row["nid"]: row["suggestion"] for row in rows}, database) + def test_symbol_files_and_stdin_compute_nids_without_a_database(self): + with tempfile.TemporaryDirectory() as directory: + names = Path(directory) / "symbols.txt" + symbols = ["sceExample", "-leading-dash", " literal spaces ", "unicode_Ω", "sceExample"] + names.write_bytes(("\ufeff" + "\r\n".join(symbols) + "\r\n\r\n").encode("utf-8")) + for source in (str(names), "-"): + with self.subTest(source=source), patch("sys.stdin", io.StringIO(names.read_text(encoding="utf-8-sig"))), \ + patch.object(nid_names, "load_db", side_effect=AssertionError("database used")), \ + contextlib.redirect_stdout(io.StringIO()) as output: + self.assertEqual(nid_names.main(["--name", "first", "--from-file", source, "--json"]), 0) + rows = json.loads(output.getvalue())["symbols"] + self.assertEqual(rows, [{"name": name, "nid": nid_names.compute_nid(name)} for name in ["first", *symbols]]) + with contextlib.redirect_stderr(io.StringIO()), self.assertRaises(SystemExit) as exited: + nid_names.main(["--from-file", str(names), "--nid"]) + self.assertEqual(exited.exception.code, 2) + with patch.object(nid_names, "load_db", side_effect=AssertionError("database used")), \ + contextlib.redirect_stdout(io.StringIO()) as output, \ + contextlib.redirect_stderr(io.StringIO()), self.assertRaises(SystemExit) as exited: + nid_names.main(["--from-file", str(names.parent / "missing"), "--json"]) + self.assertEqual(exited.exception.code, 2) + self.assertEqual(output.getvalue(), "") + + class SourceCollectionTests(unittest.TestCase): def setUp(self): @@ -282,6 +305,16 @@ auto example = "APS5_ASM_FUNCTION(\"sceQuoted_nid_postfix\")"; "sigAsmCall", "sigAsmCall_nid_postfix", }) + def test_disambiguated_definitions_are_real_names_of_their_symbol(self): + self.write_source('int APS5_VABI fstat_nid_disambig1_nid_postfix(int fd) { return 0; }\n' + 'int APS5_VABI pwrite_nid_disambig12(int fd) { return 0; }\n' + 'int APS5_VABI sceKeep_nid_disambigA() { return 0; }\n') + self.assertEqual(nid_names.collect_real_names(), { + "fstat", "fstat_nid_disambig1_nid_postfix", + "pwrite", "pwrite_nid_disambig12", + "sceKeep_nid_disambigA", + }) + def test_formatted_alias_macro_is_detected(self): self.write_source('APS5_EXPORT (\n "AAAAAAAAAAA" ,\n exampleUnknown00\n );\n') self.assertEqual(nid_names.collect_unknowns(), {"AAAAAAAAAAA": "core/libs/prx/libSceExample/Export.cpp:exampleUnknown00"}) diff --git a/tools/tests/test_package_release.py b/tools/tests/test_package_release.py index b3487ae20c..4abcb1f37b 100644 --- a/tools/tests/test_package_release.py +++ b/tools/tests/test_package_release.py @@ -49,6 +49,18 @@ class ReleasePackageTests(unittest.TestCase): self.addCleanup(patch.stopall) patch.object(package_release, "ROOT", self.source, create=True).start() + def test_ajm_vibration_decoder_license_in_archives(self): + (self.source / "core/libs/prx/libSceAjm.native").mkdir() + (self.build / "core/libs/libs/libSceAjm.native.prx").write_bytes(b"ajm") + license_file = self.source / "3rdparty/vgmstream/COPYING" + license_file.parent.mkdir(parents=True) + license_file.write_bytes(b"decoder license") + package_release.package("linux", self.build, self.output, "v1") + with zipfile.ZipFile(self.output / "prx-linux-v1.zip") as archive: + self.assertEqual(archive.read("libs/licenses/vgmstream.txt"), b"decoder license") + with tarfile.open(self.output / "prx-linux-v1.tar.gz") as archive: + self.assertEqual(archive.extractfile("libs/licenses/vgmstream.txt").read(), b"decoder license") + def test_windows_runtime_follows_the_configured_compiler_directory(self): runtime = self.root / "custom toolchain" / "bin" runtime.mkdir(parents=True) @@ -82,6 +94,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 +579,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_pad_input.py b/tools/tests/test_pad_input.py index 01b48a9e68..76e820c325 100644 --- a/tools/tests/test_pad_input.py +++ b/tools/tests/test_pad_input.py @@ -61,6 +61,10 @@ class PadInputTests(unittest.TestCase): self.assertEqual(command[1:], [64, 300]) self.assertEqual(self.queue(1001, "cross"), [command[0] + 1, 16384, 300]) + def test_timeline_command_is_advanced_after_clock_reset(self): + self.path.write_text("APS5_PAD_TIMELINE 1 2000 1\n0 100 16384\n", encoding="utf-8") + self.assertEqual(self.queue(1000, "down"), [2001, 64, 300]) + def test_equal_clock_still_produces_new_pulses(self): first = self.queue(1000, "cross") second = self.queue(1000, "down") @@ -95,7 +99,9 @@ class PadInputTests(unittest.TestCase): "18446744073709551616 16384 300\n", "2000 1 300\n", "2000 16384 0\n", "2000 16384 5001\n", "2000 16384 300 0 256 128 128\n", "2000 16384 300 extra\n", "2_000 16384 300\n", "-2000 16384 300\n", - "0 16384 300\n", " " * 128 + "2000 16384 300\n"]: + "0 16384 300\n", " " * 128 + "2000 16384 300\n", + "APS5_PAD_TIMELINE 2 2000 1\n", "APS5_PAD_TIMELINE 1 0 1\n", "APS5_PAD_TIMELINE 1 2000 0\n", + "APS5_PAD_TIMELINE 1 2000 4097\n", "APS5_PAD_TIMELINE 1 2000\n"]: with self.subTest(text=text): self.path.write_text(text, encoding="utf-8") self.assertEqual(self.queue(1000, "down"), [1000, 64, 300]) diff --git a/tools/tests/test_pad_timeline.py b/tools/tests/test_pad_timeline.py new file mode 100644 index 0000000000..cc40a5b7d8 --- /dev/null +++ b/tools/tests/test_pad_timeline.py @@ -0,0 +1,160 @@ +import contextlib +import copy +import io +import json +import os +import stat +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest +from unittest import mock + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import pad_input +import pad_timeline + + +class TimelineTests(unittest.TestCase): + def setUp(self): + self.document = {"version": 1, "events": [ + {"at_ms": 200, "hold_ms": 100, "buttons": ["cross", "r1"]}, + {"at_ms": 500, "hold_ms": 200, "buttons": ["down"]}, + ]} + + def test_compilation_and_repetition(self): + text = pad_timeline.compile_timeline(self.document, 42, 2, 300) + self.assertEqual(text, "APS5_PAD_TIMELINE 1 42 4\n200 100 18432\n500 200 64\n1200 100 18432\n1500 200 64\n") + + def test_touchpad_half_names_keep_their_command_bits(self): + self.document["events"][0]["buttons"] = ["cross", "touch-left", "touch-right"] + self.assertIn("200 100 6307840", pad_timeline.compile_timeline(self.document, 1)) + + def test_empty_buttons_release(self): + self.document["events"][0]["buttons"] = [] + self.assertIn("200 100 0", pad_timeline.compile_timeline(self.document, 1)) + + def test_release_cannot_be_combined(self): + self.document["events"][0]["buttons"] = ["release", "cross"] + with self.assertRaises(ValueError): + pad_timeline.compile_timeline(self.document, 1) + + def test_unknown_buttons(self): + for buttons in [["typo"], "cross", [True]]: + with self.subTest(buttons=buttons): + self.document["events"][0]["buttons"] = buttons + with self.assertRaises(ValueError): + pad_timeline.compile_timeline(self.document, 1) + + def test_overlaps(self): + self.document["events"][1]["at_ms"] = 250 + with self.assertRaises(ValueError): + pad_timeline.compile_timeline(self.document, 1) + + def test_integer_bounds(self): + for field, values in {"at_ms": [-1, True, 1.5, 3600001], "hold_ms": [0, True, 5001]}.items(): + for value in values: + with self.subTest(field=field, value=value): + document = copy.deepcopy(self.document) + document["events"][0][field] = value + with self.assertRaises(ValueError): + pad_timeline.compile_timeline(document, 1) + + def test_schema_validation(self): + for document in [None, [], {}, {"version": True, "events": []}, {"version": 2, "events": []}, + {"version": 1, "events": []}, {"version": 1, "events": [{}]}, + dict(self.document, unknown=1)]: + with self.subTest(document=document): + with self.assertRaises(ValueError): + pad_timeline.compile_timeline(document, 1) + + def test_repetition_bounds(self): + for repeat, gap in [(0, 0), (4097, 0), (True, 0), (1, -1), (4096, 3600000)]: + with self.assertRaises(ValueError): + pad_timeline.compile_timeline(self.document, 1, repeat, gap) + + def test_sequence_bounds(self): + for sequence in [0, -1, True, 2**64]: + with self.assertRaises(ValueError): + pad_timeline.compile_timeline(self.document, sequence) + + def test_maximum_duration(self): + document = {"version": 1, "events": [{"at_ms": 3599999, "hold_ms": 1, "buttons": ["cross"]}]} + self.assertIn("3599999 1", pad_timeline.compile_timeline(document, 2**64 - 1)) + with self.assertRaises(ValueError): + pad_timeline.compile_timeline(document, 1, 2) + + @unittest.skipUnless(os.name == "posix", "POSIX file permissions") + def test_replacement_preserves_shared_command_permissions(self): + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "command" + path.write_text("previous\n", encoding="utf-8") + path.chmod(0o640) + metadata = path.stat() + pad_timeline.queue_timeline(path, "new\n") + self.assertEqual(path.read_bytes(), b"new\n") + self.assertEqual(stat.S_IMODE(path.stat().st_mode), 0o640) + self.assertEqual((path.stat().st_uid, path.stat().st_gid), (metadata.st_uid, metadata.st_gid)) + + def test_atomic_replace_and_cleanup(self): + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "command" + pad_timeline.queue_timeline(path, "new\n") + self.assertEqual(path.read_bytes(), b"new\n") + with mock.patch("pad_timeline.os.replace", side_effect=OSError("denied")): + with self.assertRaises(OSError): + pad_timeline.queue_timeline(path, "wrong\n") + self.assertEqual(path.read_bytes(), b"new\n") + self.assertEqual(list(Path(directory).iterdir()), [path]) + + def run_cli(self, timeline, output): + return subprocess.run([sys.executable, str(Path(pad_timeline.__file__)), str(timeline), str(output)], + capture_output=True, text=True, timeout=5) + + def test_cli_roundtrip_and_invalid_input_preservation(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + timeline, output = root / "sequence.json", root / "command" + timeline.write_text(json.dumps(self.document), encoding="utf-8") + self.assertEqual(self.run_cli(timeline, output).returncode, 0) + original = output.read_bytes() + timeline.write_text('{"version":2}', encoding="utf-8") + self.assertEqual(self.run_cli(timeline, output).returncode, 2) + self.assertEqual(output.read_bytes(), original) + + def test_input_alias_protection(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + timeline = root / "sequence.json" + original = json.dumps(self.document) + timeline.write_text(original, encoding="utf-8") + self.assertEqual(self.run_cli(timeline, timeline).returncode, 2) + linked = root / "hardlink" + os.link(timeline, linked) + self.assertEqual(self.run_cli(timeline, linked).returncode, 2) + self.assertEqual(timeline.read_text(encoding="utf-8"), original) + + def test_sequence_advances_past_existing_command_after_clock_reset(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + timeline, output = root / "sequence.json", root / "command" + timeline.write_text(json.dumps(self.document), encoding="utf-8") + with mock.patch.object(sys, "argv", ["pad_timeline.py", str(timeline), str(output)]), \ + mock.patch.object(pad_input.time, "monotonic_ns", return_value=1000), \ + contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()): + output.write_text("2000 16384 300\n", encoding="utf-8") + pad_timeline.main() + self.assertTrue(output.read_text(encoding="utf-8").startswith("APS5_PAD_TIMELINE 1 2001 2\n")) + pad_timeline.main() + self.assertTrue(output.read_text(encoding="utf-8").startswith("APS5_PAD_TIMELINE 1 2002 2\n")) + exhausted = f"{2**64 - 1} 16384 300\n" + output.write_text(exhausted, encoding="utf-8") + with self.assertRaises(SystemExit) as error: + pad_timeline.main() + self.assertEqual(error.exception.code, 2) + self.assertEqual(output.read_text(encoding="utf-8"), exhausted) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_performance_compare.py b/tools/tests/test_performance_compare.py index 4b27dd7159..1cabac3fe2 100644 --- a/tools/tests/test_performance_compare.py +++ b/tools/tests/test_performance_compare.py @@ -22,6 +22,10 @@ SUMMARY = { } +IDENTITY = {"cpu": 'CPU "X"', "cpu_threads": 16, "memory_mib": 32000, + "title_id": "PPSA00000", "title": "Example", "content_version": "01.002.000"} + + class PerformanceCompareTests(unittest.TestCase): def setUp(self): self.directory = tempfile.TemporaryDirectory() @@ -119,7 +123,9 @@ class PerformanceCompareTests(unittest.TestCase): notes = "\n".join(report["notes"]) self.assertIn("state is unknown", notes) self.assertIn("missing configuration: gpu, driver", notes) - self.assertIn("do not record the CPU, title or scene", notes) + self.assertIn("cannot verify the physical machine or scene", notes) + self.assertIn("cpu", notes) + self.assertIn("title_id", notes) def test_invalid_metric_and_metadata_shapes_are_rejected_with_location(self): cases = [(("presents",), value) for value in (0, -1, True, 1.5, 10 ** 400)] @@ -129,6 +135,10 @@ class PerformanceCompareTests(unittest.TestCase): (("window", "first_present"), True), (("env",), []), (("env",), {"APS5_TRACE_FRAMES": 4}), (("trace_dropped",), "false"), (("dirty",), 1), (("trace",), None), (("top_phases_ms_per_frame",), []), (("top_phases_ms_per_frame",), {"phase": -1})] + cases += [((field,), value) for field in ("cpu", "title_id", "title", "content_version") + for value in (None, True, 12, [])] + cases += [((field,), value) for field in ("cpu_threads", "memory_mib") + for value in (None, True, -1, 1.5, "16")] for path, value in cases: with self.subTest(path=path, value=value): invalid = copy.deepcopy(self.baseline) @@ -201,6 +211,111 @@ class PerformanceCompareTests(unittest.TestCase): self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual(json.loads(result.stdout)["baseline"], self.baseline) + def test_matching_recorded_machine_and_title_context_is_accepted(self): + self.baseline.update(IDENTITY) + self.candidate.update(IDENTITY) + self.write(self.before, self.baseline) + self.write(self.after, self.candidate) + result = self.run_cli("--require-matching-context", "--json") + self.assertEqual(result.returncode, 0, result.stderr) + report = json.loads(result.stdout) + self.assertEqual(report["configuration_differences"], {}) + self.assertEqual(report["baseline"], self.baseline) + self.assertEqual(report["candidate"], self.candidate) + self.assertNotIn("missing configuration", "\n".join(report["notes"])) + for key in ("cpu_threads", "memory_mib"): + with self.subTest(unknown=key): + self.baseline[key] = self.candidate[key] = 0 + performance_compare.validate(self.baseline) + report = performance_compare.compare(self.baseline, self.candidate) + self.assertEqual(report["configuration_differences"], {}) + self.assertIn(key, "\n".join(report["notes"])) + + def test_recorded_identity_differences_are_reported_and_rejected_in_strict_mode(self): + self.baseline.update(IDENTITY) + self.write(self.before, self.baseline) + changes = {"cpu": "Other CPU", "cpu_threads": 8, "memory_mib": 16000, + "title_id": "PPSA00001", "title": "Other title", "content_version": "01.003.000"} + for key, value in changes.items(): + with self.subTest(key=key): + candidate = {**self.candidate, **IDENTITY, key: value} + expected = {key: {"baseline": IDENTITY[key], "candidate": value}} + self.assertEqual(performance_compare.compare(self.baseline, candidate)["configuration_differences"], expected) + self.write(self.after, candidate) + result = self.run_cli("--json") + self.assertEqual(result.returncode, 0, result.stderr) + report = json.loads(result.stdout) + self.assertEqual(report["configuration_differences"], expected) + self.assertEqual(report["metrics_ms"]["frame_ms.p50"]["change"], 0) + result = self.run_cli("--require-matching-context", "--json") + self.assertEqual(result.returncode, 2, result.stderr) + self.assertEqual(result.stdout, "") + self.assertIn(f"recorded configuration differs: {key}", result.stderr) + self.assertNotIn("Traceback", result.stderr) + + def test_missing_identity_is_distinct_from_known_identity_but_legacy_summaries_still_work(self): + for key in IDENTITY: + for label in ("baseline", "candidate"): + with self.subTest(key=key, missing=label): + baseline = {**self.baseline, **IDENTITY} + candidate = {**self.candidate, **IDENTITY} + (baseline if label == "baseline" else candidate).pop(key) + report = performance_compare.compare(baseline, candidate) + self.assertEqual(report["configuration_differences"], + {key: {"baseline": baseline.get(key), "candidate": candidate.get(key)}}) + self.assertIn(f"{label}: missing configuration: {key}.", report["notes"]) + self.write(self.before, self.baseline) + self.write(self.after, self.candidate) + result = self.run_cli("--require-matching-context", "--json") + self.assertEqual(result.returncode, 0, result.stderr) + report = json.loads(result.stdout) + self.assertEqual(report["configuration_differences"], {}) + self.assertIn("cpu", "\n".join(report["notes"])) + self.assertIn("title_id", "\n".join(report["notes"])) + self.write(self.after, {**self.candidate, **IDENTITY}) + result = self.run_cli("--require-matching-context") + self.assertEqual(result.returncode, 2, result.stderr) + self.assertEqual(result.stdout, "") + for key in IDENTITY: + self.assertIn(key, result.stderr) + + def test_recorded_identity_differences_are_escaped_in_markdown(self): + self.baseline.update(IDENTITY) + self.candidate.update(IDENTITY, cpu="CPU | <\u03a9>\nline", title="Other title", content_version="02.000.000") + self.write(self.before, self.baseline) + self.write(self.after, self.candidate) + result = self.run_cli() + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("CPU | <\u03a9> line", result.stdout) + self.assertIn("| title | Example | Other title |", result.stdout) + self.assertIn("| content_version | 01.002.000 | 02.000.000 |", result.stdout) + + def test_matching_context_can_be_required_without_a_timing_verdict(self): + self.write(self.before, self.baseline) + self.candidate["frame_ms"]["max"] = 100.0 + self.write(self.after, self.candidate) + result = self.run_cli("--require-matching-context", "--json") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(json.loads(result.stdout)["metrics_ms"]["frame_ms.max"]["change"], 60.0) + for field in ("gpu", "driver", "os", "resolution"): + with self.subTest(field=field): + candidate = copy.deepcopy(self.candidate) + candidate[field] += " changed" + self.write(self.after, candidate) + result = self.run_cli("--require-matching-context") + self.assertEqual(result.returncode, 2, result.stderr) + self.assertIn(field, result.stderr) + self.assertEqual(result.stdout, "") + self.assertEqual(self.run_cli().returncode, 0) + for key, value in (("window", dict(self.candidate["window"], first_present=900)), + ("env", {"APS5_TRACE_FRAMES": "900:4"})): + with self.subTest(key=key): + candidate = copy.deepcopy(self.candidate) + candidate[key] = value + self.write(self.after, candidate) + self.assertEqual(self.run_cli("--require-matching-context").returncode, 2) + + if __name__ == "__main__": unittest.main() 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_dependencies.py b/tools/tests/test_pr_overlap_dependencies.py index bcff60153c..11754c81b7 100644 --- a/tools/tests/test_pr_overlap_dependencies.py +++ b/tools/tests/test_pr_overlap_dependencies.py @@ -94,6 +94,12 @@ class DependencyTests(unittest.TestCase): with self.subTest(body=body): self.assertEqual(pull_requests(body)[1]["depends"], expected) + def test_empty_colon_declaration_does_not_hide_a_later_one(self): + for body in ("- [ ] Depends on: <!-- #PR, or none -->\n\nDepends on: #2, #3", + "Depends on:\n\nDepends on: #2 and #3"): + with self.subTest(body=body): + self.assertEqual(pull_requests(body)[1]["depends"], {2, 3}) + def test_checklist_none_wins_over_prose_with_placeholder_comment(self): body = "Depends on #2.\n- [x] Depends on: none <!-- #PR, or none -->" self.assertEqual(pull_requests(body)[1]["depends"], set()) 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_pr_overlap_exports.py b/tools/tests/test_pr_overlap_exports.py index eef9292aa7..d326656d8b 100644 --- a/tools/tests/test_pr_overlap_exports.py +++ b/tools/tests/test_pr_overlap_exports.py @@ -25,7 +25,16 @@ diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkerne class ExportTests(unittest.TestCase): def test_added_definitions_are_exports(self): - self.assertEqual(pr_overlap.exports(DIFF), {"mlock_nid_postfix", "sceKernelRead", "getpid_nid_postfix", "sceNpUniversalDataSystemCreateEvent"}) + self.assertEqual(pr_overlap.exports(DIFF), {"mlock", "sceKernelRead", "getpid", "sceNpUniversalDataSystemCreateEvent"}) + + def test_nid_postfix_aliases_match_plain_export_names(self): + suffixed = "+int APS5_VABI sceExample_nid_postfix() { return 0; }\n" + plain = "+int APS5_VABI sceExample() { return 0; }\n" + self.assertEqual(pr_overlap.exports(suffixed), pr_overlap.exports(plain)) + + def test_nid_disambiguation_suffix_is_normalized(self): + diff = "+int APS5_VABI sceExample_nid_disambig12_nid_postfix() { return 0; }\n" + self.assertEqual(pr_overlap.exports(diff), {"sceExample"}) def test_cut_helpers_are_not_export_overlaps(self): diff = DIFF + '+void APS5_VABI Helper_nid_no_patch_cut() {\n' diff --git a/tools/tests/test_progress.py b/tools/tests/test_progress.py index 84c3479696..1db231c27f 100644 --- a/tools/tests/test_progress.py +++ b/tools/tests/test_progress.py @@ -10,6 +10,102 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[1])) import progress +class ProgressSnapshotTests(unittest.TestCase): + def snapshot(self): + return {"libraries": {"percent": 0, "groups": []}, "shaders": {"percent": 0, "groups": []}} + + def assert_rejected(self, snapshot, message): + for position in (0, 1): + with self.subTest(position=position), tempfile.TemporaryDirectory() as directory: + paths = [Path(directory) / "base snapshot.json", Path(directory) / "head snapshot.json"] + snapshots = [self.snapshot(), self.snapshot()] + snapshots[position] = snapshot + for path, data in zip(paths, snapshots): + path.write_text(json.dumps(data), encoding="utf-8") + stdout, stderr = io.StringIO(), io.StringIO() + with patch("sys.stdout", stdout), patch("sys.stderr", stderr): + with self.assertRaises(SystemExit) as raised: + progress.main(["--compare", *map(str, paths)]) + self.assertEqual(raised.exception.code, 2) + self.assertEqual(stdout.getvalue(), "") + self.assertIn(str(paths[position]), stderr.getvalue()) + self.assertIn(message, stderr.getvalue()) + self.assertNotIn("Traceback", stderr.getvalue()) + + def test_non_object_snapshots_are_usage_errors(self): + for snapshot in (None, [], "progress", 7, True): + with self.subTest(snapshot=snapshot): + self.assert_rejected(snapshot, "snapshot must be an object") + + def test_sections_must_be_present_objects(self): + for section in ("libraries", "shaders"): + for value in (None, [], "progress", 7): + with self.subTest(section=section, value=value): + snapshot = self.snapshot() + snapshot[section] = value + self.assert_rejected(snapshot, section + " must be an object") + snapshot = self.snapshot() + del snapshot[section] + self.assert_rejected(snapshot, section + " must be an object") + + def test_groups_must_be_lists_of_named_objects(self): + for section in ("libraries", "shaders"): + for groups in (None, {}, "groups", [None], ["group"], [{}], [{"name": []}]): + with self.subTest(section=section, groups=groups): + snapshot = self.snapshot() + snapshot[section]["groups"] = groups + self.assert_rejected(snapshot, section + ".groups") + snapshot = self.snapshot() + del snapshot[section]["groups"] + self.assert_rejected(snapshot, section + ".groups") + + def test_symbol_lists_cannot_be_strings_or_unhashable_values(self): + for field in ("done_names", "todo_names", "inert_names"): + for value in ("Ready", None, {}, [7], [["Ready"]]): + with self.subTest(field=field, value=value): + snapshot = self.snapshot() + snapshot["libraries"]["groups"] = [{"name": "libSceExample", field: value}] + self.assert_rejected(snapshot, "." + field + " must be a list of strings") + + def test_shared_source_owners_must_be_strings(self): + for value in (None, [], {}, 7, True): + with self.subTest(value=value): + snapshot = self.snapshot() + snapshot["libraries"]["groups"] = [{"name": "libSceExample.native", "shared_sources": value}] + self.assert_rejected(snapshot, ".shared_sources must be a string") + + def test_percentages_must_be_finite_numbers_in_range(self): + for section in ("libraries", "shaders"): + for value in (None, "0", True, [], {}, -1, 101, float("nan"), float("inf"), -float("inf")): + with self.subTest(section=section, value=value): + snapshot = self.snapshot() + snapshot[section]["percent"] = value + self.assert_rejected(snapshot, section + ".percent") + snapshot = self.snapshot() + del snapshot[section]["percent"] + self.assert_rejected(snapshot, section + ".percent") + + def test_valid_snapshots_keep_reports_and_optional_fields_compatible(self): + base, head = self.snapshot(), self.snapshot() + base["libraries"]["groups"] = [{"name": "libSceExample", "todo_names": ["Ready"]}, {"name": "libEmpty"}] + head["libraries"]["groups"] = [{"name": "libSceExample", "done_names": ["Ready"], "future_field": True}, + {"name": "libEmpty"}, {"name": "libSceExample.native", "shared_sources": "libSceExample"}] + head["libraries"]["percent"] = 100 + expected = progress.report(base, head) + self.assertIn("Ready", expected) + with tempfile.TemporaryDirectory() as directory: + paths = [Path(directory) / "base.json", Path(directory) / "head.json"] + for path, snapshot in zip(paths, (base, head)): + path.write_text(json.dumps(snapshot), encoding="utf-8") + stdout = io.StringIO() + with patch("sys.stdout", stdout): + self.assertEqual(progress.main(["--compare", *map(str, paths)]), 0) + self.assertEqual(stdout.getvalue(), expected) + with patch("sys.stdout", stdout): + self.assertEqual(progress.main(["--compare", str(paths[0]), str(paths[0])]), 0) + self.assertEqual(stdout.getvalue(), expected) + + class ProgressTests(unittest.TestCase): def test_raw_string_example_does_not_declare_an_export(self): source = 'const char* example = R"help(\nint APS5_VABI Fake() { return 0; }\n)help";\n' @@ -387,5 +483,34 @@ 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()) + + def test_root_must_be_a_source_tree(self): + with tempfile.TemporaryDirectory() as directory: + empty = Path(directory) / "empty tree" + empty.mkdir() + half = Path(directory) / "half tree" + (half / "core" / "libs" / "prx").mkdir(parents=True) + for name, path in (("missing", Path(directory) / "missing tree"), ("empty", empty), ("no opcodes", half)): + with self.subTest(name): + stderr = io.StringIO() + with patch("sys.stderr", stderr), self.assertRaises(SystemExit) as raised: + progress.main(["--json", "--root", str(path)]) + self.assertEqual(raised.exception.code, 2) + self.assertIn("is not a source tree", stderr.getvalue()) + 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 b467e9af88..813b596990 100644 --- a/tools/tests/test_progress_history.py +++ b/tools/tests/test_progress_history.py @@ -14,19 +14,74 @@ 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) class ProgressHistoryTests(unittest.TestCase): + def test_cache_reuses_immutable_commits_and_invalidates_changed_rules(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + subprocess.run(["git", "-C", directory, "init", "-q"], check=True) + commit(root, "2026-01-01", "first", "int APS5_VABI A() { return 0; }\n") + cache = root / "cache" / "history.json" + isa = root / "isa.txt" + isa.write_bytes(progress.ISA.read_bytes()) + with patch.multiple(progress, ROOT=root, PRX=root / "core/libs/prx", OPCODES=root / OPCODES, ISA=isa): + with patch.object(progress_history, "measure", wraps=progress_history.measure) as measure: + first = progress_history.collect("HEAD", cache) + self.assertEqual(measure.call_count, 1) + before = cache.read_bytes() + measure.reset_mock() + self.assertEqual(progress_history.collect("HEAD", cache), first) + measure.assert_not_called() + self.assertEqual(cache.read_bytes(), before) + commit(root, "2026-01-02", "second", "int APS5_VABI A() { return 0; }\nint APS5_VABI B() { return 0; }\n") + measure.reset_mock() + second = progress_history.collect("HEAD", cache) + self.assertEqual(measure.call_count, 1) + self.assertEqual(second[0], first[0]) + isa.write_bytes(isa.read_bytes() + b"\n# changed rules input\n") + measure.reset_mock() + self.assertEqual(progress_history.collect("HEAD", cache), second) + self.assertEqual(measure.call_count, 2) + uncached = progress_history.collect("HEAD") + self.assertEqual(uncached, second) + self.assertEqual(progress_history.page(uncached), progress_history.page(second)) + self.assertFalse(any(path.name.startswith(".history-") for path in cache.parent.iterdir())) + + def test_invalid_cache_is_not_overwritten(self): + import json + with tempfile.TemporaryDirectory() as directory: + cache = Path(directory) / "history.json" + for contents in (b"{", b"[]", json.dumps({"schema_version": 1, "rules": progress_history.cache_signature(), + "commits": {"a" * 40: {"libraries": {"done": True, "total": 1, "percent": 100}, + "shaders": {"done": 0, "total": 0, "percent": 0}}}}).encode()): + with self.subTest(contents=contents): + cache.write_bytes(contents) + with patch.object(progress_history, "measure", side_effect=AssertionError("measurement started")), \ + self.assertRaises(ValueError): + progress_history.collect("HEAD", cache) + self.assertEqual(cache.read_bytes(), contents) + + def test_failed_cache_publication_preserves_the_old_file(self): + with tempfile.TemporaryDirectory() as directory: + cache = Path(directory) / "history.json" + cache.write_bytes(b"previous cache") + with patch.object(progress_history.os, "replace", side_effect=OSError("cannot publish")), \ + self.assertRaisesRegex(OSError, "cannot publish"): + progress_history.save_cache(cache, "rules", {}) + self.assertEqual(cache.read_bytes(), b"previous cache") + self.assertEqual(list(cache.parent.iterdir()), [cache]) + def test_one_point_per_day_at_the_last_commit(self): stub = "static void Stub() { NotImplemented_nid_no_patch(__func__); }\n" with tempfile.TemporaryDirectory() as directory: @@ -43,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() @@ -75,5 +141,6 @@ class ProgressHistoryTests(unittest.TestCase): self.assertIn(f">{label}<", svg) + if __name__ == "__main__": unittest.main() 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 192a533da4..e86b16ccad 100644 --- a/tools/tests/test_select_tests.py +++ b/tools/tests/test_select_tests.py @@ -101,6 +101,13 @@ class SelectTests(unittest.TestCase): self.assertIsNone(selected) self.assertIn(changed, output) + def test_changed_list_with_a_byte_order_mark_and_crlf_keeps_its_first_path(self): + listed = self.build / "changed bom.txt" + listed.write_bytes(b"\xef\xbb\xbfCMakeLists.txt\r\nalpha/alpha.h\r\n") + output = self.tool("select", self.build, self.graph, listed) + self.assertIn("CMakeLists.txt", output) + self.assertFalse((self.build / "select.tests").exists()) + def test_missing_graph_runs_everything(self): self.assertIsNone(self.select("alpha/alpha.h", graph=self.build / "missing.json")[1]) @@ -119,6 +126,110 @@ class SelectTests(unittest.TestCase): subprocess.run(["cmake", "--build", str(self.build)], check=True, capture_output=True) self.assertEqual(built, {"alpha_tests"}) + def test_explain_reports_input_and_fixture_reasons_without_a_build_cache(self): + changed = self.build / "explain-changed.txt" + cache = self.build / "CMakeCache.txt" + saved = self.build / "CMakeCache.saved" + cache.rename(saved) + try: + for path in ("alpha/alpha.h", "beta/main.c", "CMakeLists.txt"): + with self.subTest(path=path): + changed.write_text(path + "\n", encoding="utf-8") + report = json.loads(self.tool("explain", self.graph, changed)) + picked, full = select_tests.select(json.loads(self.graph.read_text(encoding="utf-8")), [path]) + self.assertEqual(report["tests"], sorted(picked) if picked is not None else None) + self.assertEqual(report["full_run_paths"], full) + if path == "alpha/alpha.h": + self.assertIn("input: alpha/alpha.h", report["reasons"]["alpha"]) + self.assertIn("no build executable", report["reasons"]["script"]) + elif path == "beta/main.c": + self.assertIn("fixture setup for: beta", report["reasons"]["beta_setup"]) + finally: + saved.rename(cache) + + + +@unittest.skipUnless(shutil.which("ninja") and (shutil.which("gcc") or shutil.which("cc")), "needs ninja and a C compiler") +class FixtureBuildTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.directory = tempfile.TemporaryDirectory() + root = Path(cls.directory.name) + cls.source, cls.build, cls.graph = root / "source", root / "build", root / "graph.json" + project = dict(PROJECT) + project["CMakeLists.txt"] += "add_subdirectory(cleanup)\n" + project["cleanup/CMakeLists.txt"] = ( + "add_executable(cleanup_tests main.c)\nadd_test(NAME beta_cleanup COMMAND cleanup_tests)\n" + "set_tests_properties(beta_cleanup PROPERTIES FIXTURES_CLEANUP beta_fixture)\n") + project["cleanup/main.c"] = ('#include <stdio.h>\nint main(void) { FILE *f = fopen("cleanup.ran", "w"); ' + 'if (!f) return 1; return fclose(f) != 0; }\n') + for name, content in project.items(): + (cls.source / name).parent.mkdir(parents=True, exist_ok=True) + (cls.source / name).write_text(content, encoding="utf-8") + compiler = shutil.which("gcc") or shutil.which("cc") + subprocess.run(["cmake", "-S", str(cls.source), "-B", str(cls.build), "-G", "Ninja", f"-DCMAKE_C_COMPILER={compiler}"], + check=True, capture_output=True) + subprocess.run(["cmake", "--build", str(cls.build)], check=True, capture_output=True) + SelectTests.tool("dump", cls.build, cls.graph) + + @classmethod + def tearDownClass(cls): + cls.directory.cleanup() + + def run_selected(self, changed, graph): + subprocess.run(["cmake", "--build", str(self.build)], check=True, capture_output=True) + marker = self.build / "cleanup/cleanup.ran" + marker.unlink(missing_ok=True) + listed = self.build / "changed.txt" + listed.write_text(changed + "\n", encoding="utf-8") + SelectTests.tool("select", self.build, graph, listed) + subprocess.run(["ninja", "-C", str(self.build), "-t", "clean"], check=True, capture_output=True) + subprocess.run(["ninja", "-C", str(self.build), "-f", "select.ninja", "selected"], check=True, capture_output=True) + result = subprocess.run([sys.executable, str(TOOL), "ctest", str(self.build), "--output-on-failure"], + capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertTrue(marker.is_file(), "CTest did not execute the fixture cleanup") + return result.stdout + + def test_cleanup_executable_is_built_when_ctest_adds_it(self): + output = self.run_selected("beta/main.c", self.graph) + self.assertIn("beta_cleanup", output) + self.assertNotIn("alpha", output) + self.assertFalse(any(path.is_file() for path in (self.build / "alpha").glob("alpha_tests*"))) + + def test_new_test_gets_its_current_setup_and_cleanup(self): + graph = json.loads(self.graph.read_text(encoding="utf-8")) + del graph["tests"]["beta"] + older = self.build / "older.json" + older.write_text(json.dumps(graph), encoding="utf-8") + output = self.run_selected("alpha/main.c", older) + for name in ("alpha", "alpha_data", "beta", "beta_setup", "beta_cleanup"): + self.assertIn(name, output) + + +class BuildErrorTests(unittest.TestCase): + def run_tool(self, *args): + return subprocess.run([sys.executable, "-I", str(TOOL), *map(str, args)], capture_output=True, text=True, timeout=60) + + def test_missing_build_directory_is_reported_without_a_traceback(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + for command in (("dump", root / "no build", root / "graph.json"), + ("select", root / "no build", root / "graph.json", root / "changed.txt")): + with self.subTest(command[0]): + result = self.run_tool(*command) + self.assertEqual(result.returncode, 2) + self.assertIn("not a configured CMake build directory", result.stderr) + self.assertNotIn("Traceback", result.stderr) + + def test_cache_without_the_build_locations_is_reported_without_a_traceback(self): + with tempfile.TemporaryDirectory() as directory: + (Path(directory) / "CMakeCache.txt").write_text("# empty\n", encoding="utf-8") + result = self.run_tool("dump", directory, Path(directory) / "graph.json") + self.assertEqual(result.returncode, 2) + self.assertIn("CMAKE_CACHEFILE_DIR", result.stderr) + self.assertNotIn("Traceback", result.stderr) + class CaseInsensitivePathTests(unittest.TestCase): def build(self): @@ -146,5 +257,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..106d3bc3ae 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,74 @@ 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) + +class ScriptRegistrationTests(unittest.TestCase): + def unrun(self, files): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + check_test_registration.subprocess.run(["git", "init", "-q", str(root)], check=True) + for name, content in files.items(): + (root / name).parent.mkdir(parents=True, exist_ok=True) + (root / name).write_text(content, encoding="utf-8") + return check_test_registration.unrun_scripts(root)[1] + + def test_script_registered_by_a_nested_add_test_runs(self): + self.assertEqual(self.unrun({ + "core/relinker/CMakeLists.txt": "if(WIN32)\n add_test(NAME a COMMAND ${Python3_EXECUTABLE}\n ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_a.py)\nendif()\n", + "core/relinker/relinker/tests/test_a.py": ""}), []) + + def test_unregistered_script_is_reported(self): + self.assertEqual(self.unrun({ + "CMakeLists.txt": "add_test(NAME a COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_a.py)\n", + "tools/tests/test_a.py": "", "tools/tests/test_b.py": "", "tools/tests/helper.py": ""}), ["tools/tests/test_b.py"]) + + def test_commented_add_test_does_not_run_a_script(self): + self.assertEqual(self.unrun({ + "CMakeLists.txt": "#add_test(NAME a COMMAND python ${CMAKE_CURRENT_SOURCE_DIR}/test_a.py)\n#[[\nadd_test(NAME a COMMAND python test_a.py)\n]]\n", + "test_a.py": ""}), ["test_a.py"]) + + def test_registration_names_its_directory(self): + self.assertEqual(self.unrun({ + "CMakeLists.txt": "add_test(NAME a COMMAND python ${CMAKE_CURRENT_SOURCE_DIR}/a/tests/test_a.py)\n", + "a/tests/test_a.py": "", "b/tests/test_a.py": ""}), ["b/tests/test_a.py"]) + + def test_workflow_run_counts_and_a_paths_filter_does_not(self): + self.assertEqual(self.unrun({ + ".github/workflows/pages.yml": "jobs:\n web:\n steps:\n - run: python3 tools/test_web.py --build build-wasm\n", + ".github/workflows/web.yml": "on:\n pull_request:\n paths:\n - tools/test_paths.py\n", + "tools/test_web.py": "", "tools/test_paths.py": ""}), ["tools/test_paths.py"]) + + def test_commented_workflow_line_does_not_run_a_script(self): + self.assertEqual(self.unrun({ + ".github/workflows/ci.yml": "# python3 tools/tests/test_f.py\njobs:\n t:\n steps:\n # python3 tools/tests/test_a.py\n" + " - run: |\n #python3 tools/tests/test_b.py\n" + " \t# python3 tools/tests/test_c.py\n" + " echo done\t# python3 tools/tests/test_d.py\n" + " python3 tools/tests/test_e.py\n", + "tools/tests/test_a.py": "", "tools/tests/test_b.py": "", "tools/tests/test_c.py": "", + "tools/tests/test_d.py": "", "tools/tests/test_e.py": "", "tools/tests/test_f.py": ""}), + ["tools/tests/test_a.py", "tools/tests/test_b.py", "tools/tests/test_c.py", "tools/tests/test_d.py", + "tools/tests/test_f.py"]) + + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_trace_digest.py b/tools/tests/test_trace_digest.py index c0b7b67112..a7fa3619db 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") @@ -280,6 +315,63 @@ class TraceDigestTests(unittest.TestCase): self.assertAlmostEqual(waits[0]["ms"], 14.5) self.assertEqual(waits[1]["name"], "Driver.WorkerWait") + def test_longest_waits_omit_slices_outside_capture_in_cli(self): + events = [EVENTS[1], *EVENTS[-3:], + complete(1, "Inside.wait", 1000, 2000), + complete(3, "Before.wait", -100000, 90000), + complete(4, "After.wait", 40000, 90000), + complete(5, "EndsAtStart.wait", -1000, 1000), + complete(6, "StartsAtEnd.wait", 40000, 1000)] + plain = self.trace.with_suffix("") + plain.write_text(json.dumps({"traceEvents": events}), encoding="utf-8") + self.trace.write_bytes(gzip.compress(plain.read_bytes())) + for path in (self.trace, plain): + for output in ([], ["--json"]): + with self.subTest(path=path.name, output=output): + result = subprocess.run([sys.executable, trace_digest.__file__, str(path), "--top", "1", *output], + capture_output=True, text=True, encoding="utf-8", timeout=20) + self.assertEqual((result.returncode, result.stderr), (0, "")) + if output: + report = json.loads(result.stdout) + self.assertEqual(report["longest_waits"], [ + {"name": "Inside.wait", "thread": "queue worker 0x0", "ms": 2.0, "at_ms": 1.0, "present": 600}]) + else: + self.assertIn("- Inside.wait (queue worker 0x0) 2.0 ms at 1 ms, present 600", result.stdout) + for name in ("Before.wait", "After.wait", "EndsAtStart.wait", "StartsAtEnd.wait"): + self.assertNotIn(name, result.stdout) + + def test_longest_waits_rank_only_the_overlap_with_capture(self): + events = [dict(EVENTS[-3], ts=10000), dict(EVENTS[-2], ts=30000), + complete(1, "Leading.wait", -100000, 111000), + complete(2, "Inside.wait", 12000, 4000), + complete(3, "Trailing.wait", 28000, 90000), + complete(4, "Spanning.wait", -100000, 230000), + complete(4, "Nested.wait", 11000, 1000)] + report = trace_digest.digest(trace_digest.Trace(events), top=4) + self.assertEqual(report["longest_waits"], [ + {"name": "Spanning.wait", "thread": "tid 4", "ms": 20.0, "at_ms": 10.0, "present": 600}, + {"name": "Inside.wait", "thread": "tid 2", "ms": 4.0, "at_ms": 12.0, "present": 600}, + {"name": "Trailing.wait", "thread": "tid 3", "ms": 2.0, "at_ms": 28.0, "present": 600}, + {"name": "Leading.wait", "thread": "tid 1", "ms": 1.0, "at_ms": 10.0, "present": 600}]) + threads = {thread["thread"]: thread for thread in report["threads"]} + for wait in report["longest_waits"]: + self.assertEqual(wait["ms"], threads[wait["thread"]]["wait_ms_per_frame"]) + + def test_longest_waits_clip_to_summary_end(self): + trace = trace_digest.Trace([EVENTS[-3], + complete(1, "Trailing.wait", 8000, 20000), + complete(2, "Inside.wait", 1000, 3000)]) + report = trace_digest.digest(trace, {"window": {"ms": 10.0}}, top=1) + self.assertEqual(report["longest_waits"], [ + {"name": "Inside.wait", "thread": "tid 2", "ms": 3.0, "at_ms": 1.0, "present": 600}]) + + def test_longest_waits_without_frames_keep_the_full_slices(self): + for duration in (0, 3000): + with self.subTest(duration=duration): + trace = trace_digest.Trace([complete(1, "Worker.wait", -1000, duration)]) + self.assertEqual(trace_digest.digest(trace)["longest_waits"], [ + {"name": "Worker.wait", "thread": "tid 1", "ms": duration / 1000.0, "at_ms": -1.0, "present": None}]) + def test_wait_subscope_without_parent_is_not_busy(self): events = [EVENTS[1], complete(1, "Driver.WorkerWait.dequeue_order", 0, 4000), complete(1, "Driver.Packet", 4000, 5000), EVENTS[-3], EVENTS[-2]] @@ -462,6 +554,36 @@ class TraceDigestTests(unittest.TestCase): self.assertEqual(raised.exception.code, 2) self.assertIn("summary file not found", error.getvalue()) + def test_thread_filter_keeps_frame_bounds_and_capture_summary(self): + command = [sys.executable, trace_digest.__file__, str(self.trace)] + result = subprocess.run([*command, "--thread", "queue worker 0x0", "--json"], capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + report = json.loads(result.stdout) + self.assertEqual(report["frames"], 3) + self.assertEqual(report["summary"], SUMMARY) + self.assertEqual(report["thread_filter"], ["queue worker 0x0"]) + self.assertEqual([thread["thread"] for thread in report["threads"]], ["queue worker 0x0"]) + self.assertTrue(all("Present" not in entry["name"] for entry in report["top_slices_ms_per_frame"])) + self.assertTrue(all(wait["thread"] == "queue worker 0x0" for wait in report["longest_waits"])) + result = subprocess.run([*command, "--thread", "presenter", "--thread", "queue worker 0x0", "--json"], + capture_output=True, text=True) + unfiltered = trace_digest.digest(trace_digest.load_trace(self.trace), SUMMARY) + report = json.loads(result.stdout) + del report["thread_filter"] + self.assertEqual(report, unfiltered) + result = subprocess.run([*command, "--thread", "presenter"], capture_output=True, text=True) + self.assertIn("Thread filter:", result.stdout) + self.assertIn("capture-wide", result.stdout) + result = subprocess.run([*command, "--thread", "misspelled"], capture_output=True, text=True) + self.assertEqual(result.returncode, 2) + self.assertIn("unknown thread", result.stderr) + self.assertEqual(result.stdout, "") + result = subprocess.run([sys.executable, trace_digest.__file__, str(self.trace.with_suffix(".jsonl")), + "--thread", "presenter"], capture_output=True, text=True) + self.assertEqual(result.returncode, 2) + self.assertIn("event trace", result.stderr) + + def interval_line(start, end, p99, gpu_ms, **changes): line = dict(SUMMARY, window={"spec": "summary_every 30", "first_present": 0, "ms": (end - start) * 1000.0}, interval_start_s=start, interval_end_s=end, @@ -484,6 +606,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"] @@ -495,6 +625,13 @@ class IntervalDigestTests(unittest.TestCase): self.assertIsNone(third["top_phase"]) self.assertEqual(result["identity_changed_at"], [3]) + def test_identity_changes_are_reported_where_they_happen(self): + lines = [interval_line(30.0 * index, 30.0 * (index + 1), 20.0, 5.0, commit=commit) + for index, commit in enumerate(("a", "a", "b", "b", "a"))] + result = trace_digest.digest_intervals(lines) + self.assertEqual(result["identity_changed_at"], [2, 4]) + self.assertIn("commit, environment, GPU, driver or OS changed at interval 3, 5", trace_digest.markdown_intervals(result).splitlines()) + def test_sparkline_scales_to_the_largest_value(self): self.assertEqual(trace_digest.sparkline([0.0, 5.0, 10.0]), ".+@") self.assertEqual(trace_digest.sparkline([0.0, 0.0]), "..") @@ -564,5 +701,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 28fc3a6987..6ee6d2095e 100644 --- a/tools/tests/test_unwrap_self.py +++ b/tools/tests/test_unwrap_self.py @@ -1,5 +1,6 @@ import contextlib import io +import os import struct import subprocess import sys @@ -153,6 +154,43 @@ class UnwrapTest(unittest.TestCase): self.assertEqual(tls[2], read_only[2] + 0x80) self.assertEqual(tls[5], 0) + def test_overlapping_load_sources_keep_each_load_alignment(self): + payload = b"shared load payload\0" * 16 + for delta, size in ((0, len(payload)), (0, 0x80), (0x40, 0x80)): + for recorded in (False, True): + with self.subTest(delta=delta, size=size, recorded=recorded): + source = 0x800 if recorded else 0x2000 + segments = [(PT_LOAD, 5, 0x2000, 0x1000, len(payload), PAGE), + (PT_LOAD, 6, 0x2000 + delta, 0x4000 + delta, size, 2 * PAGE)] + records = [(0x800, source, len(payload)), (0x100800, source + delta, size)] if recorded else [] + container = build_container(segments, records, {source: payload}) + result, output = self.convert(container) + self.assertEqual(result.returncode, 0, result.stderr) + data, headers = read_program_headers(output) + for header, expected in zip(headers, (payload, payload[delta:delta + size])): + self.assertEqual(header[2] % header[7], header[3] % header[7]) + self.assertEqual(data[header[2]:header[2] + header[5]], expected) + self.assertEqual((Path(self.directory) / "input.self").read_bytes(), container) + + def test_overlapping_load_sources_keep_embedded_segments_in_their_host(self): + payload = b"overlapping load payload\0" * 16 + for kind in (PT_DYNAMIC, PT_TLS): + for size in (0, 0x10): + with self.subTest(kind=kind, size=size): + segments = [(PT_LOAD, 5, 0x1000, 0x1000, 0x100, PAGE), + (PT_LOAD, 6, 0x1080, 0x3080, 0x100, PAGE), + (kind, 4, 0x1090, 0x3090, size, 8)] + container = build_container(segments, [], {0x1000: payload}) + result, output = self.convert(container) + self.assertEqual(result.returncode, 0, result.stderr) + data, headers = read_program_headers(output) + for header, start in zip(headers[:2], (0, 0x80)): + self.assertEqual(header[2] % header[7], header[3] % header[7]) + self.assertEqual(data[header[2]:header[2] + header[5]], payload[start:start + 0x100]) + host, embedded = headers[1:] + self.assertEqual(embedded[2] - host[2], embedded[3] - host[3]) + self.assertEqual(data[embedded[2]:embedded[2] + size], payload[0x90:0x90 + size]) + def test_segments_after_the_recorded_comment_are_read_from_the_container(self): segments = default_segments() + [(SCE_COMMENT, 4, 0x15A0, 0x0, 0x20, 0x10), (PT_NOTE, 4, 0x15C0, 0x0, 0x18, 4), (SCE_VERSION, 4, 0x15D8, 0x0, 0x10, 1)] @@ -196,6 +234,81 @@ 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_file_backed_load_uses_the_first_congruent_offset_after_the_headers(self): + cases = ((PROGRAM_TABLE_OFFSET, 0, 0x1400, 0x400), + (PROGRAM_TABLE_OFFSET, 1, 0x1400, 0x400), + (PROGRAM_TABLE_OFFSET, 8, 0x1003, 0x403), + (PROGRAM_TABLE_OFFSET, PAGE, 0x1400, 0x400), + (PROGRAM_TABLE_OFFSET, PAGE, 0x1480, 0x480), + (PROGRAM_TABLE_OFFSET, PAGE, 0x1200, 0x1200), + (PROGRAM_TABLE_OFFSET, PAGE, 0x1000, 0x1000), + (PROGRAM_TABLE_OFFSET, PAGE, 0x1FFF, 0xFFF), + (0xFF0, PAGE, 0x2100, 0x1100), + (PROGRAM_TABLE_OFFSET, 1 << 63, 0x480, 0x480)) + payload = pattern(3, 0x20) * 8 + for program_offset, alignment, address, expected in cases: + with self.subTest(program_offset=program_offset, alignment=alignment, address=address): + segments = [(PT_LOAD, 5, 0x1234, address, len(payload), alignment)] + container = build_container(segments, [(0x2804, 0x2000, len(payload))], + {0x2000: payload}, program_offset=program_offset) + result, output = self.convert(container) + self.assertEqual(result.returncode, 0, result.stderr) + data, headers = read_program_headers(output) + load = headers[0] + self.assertEqual(load[2], expected) + self.assertGreaterEqual(load[2], max(FIRST_REGION, program_offset + 56)) + self.assertEqual(load[2] % max(alignment, 1), address % max(alignment, 1)) + self.assertEqual(load[3:], (address, address, len(payload), len(payload), alignment)) + self.assertEqual(data[load[2]:load[2] + load[5]], payload) + self.assertEqual(len(data), expected + len(payload)) + self.assertEqual((Path(self.directory) / "input.self").read_bytes(), container) + + def test_recorded_non_load_region_can_start_at_an_already_congruent_cursor(self): + load_payload = pattern(3, 0x20) * 8 + b"tail" + separate_payload = pattern(9, 0x10) + for kind in (PT_DYNAMIC, PT_TLS): + for alignment in (8, 0x10): + with self.subTest(kind=kind, alignment=alignment): + segments = [(PT_LOAD, 5, 0x1234, 0x1000, len(load_payload), PAGE), + (kind, 4, 0x2000, 0x1004, len(separate_payload), alignment)] + records = [(0x2804, 0x800, len(load_payload)), (0x102804, 0xA00, len(separate_payload))] + container = build_container(segments, records, {0x800: load_payload, 0xA00: separate_payload}) + result, output = self.convert(container) + self.assertEqual(result.returncode, 0, result.stderr) + data, headers = read_program_headers(output) + load, separate = headers + self.assertEqual(load[2], PAGE) + self.assertEqual(separate[2], load[2] + len(load_payload)) + self.assertEqual(separate[2] % alignment, separate[3] % alignment) + self.assertEqual(data[load[2]:load[2] + load[5]], load_payload) + self.assertEqual(data[separate[2]:separate[2] + separate[5]], separate_payload) + self.assertEqual(len(data), separate[2] + len(separate_payload)) + 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)} @@ -274,6 +387,17 @@ class UnwrapTest(unittest.TestCase): self.assertNotIn("Traceback", result.stderr) self.assertEqual(output.read_bytes(), b"previous output") + def test_unsupported_inner_elf_class_and_byte_order_are_refused(self): + for offset, value in ((4, 1), (5, 2)): + with self.subTest(offset=offset, value=value): + container = bytearray(build_container(default_segments(), default_records(), default_payloads())) + container[ELF_HEADER_OFFSET + offset] = value + result, output = self.convert(bytes(container)) + self.assertEqual(result.returncode, 2) + self.assertIn("inner ELF is not 64-bit little-endian", result.stderr) + self.assertNotIn("Traceback", result.stderr) + self.assertFalse(output.exists()) + def test_a_container_whose_load_segments_look_encrypted_is_refused(self): random_like = bytes(range(256)) * 4 segments = [(PT_LOAD, 1, 0x1234, 0x0, 0x400, PAGE), (PT_LOAD, 4, 0x1500, 0x1000, 0x400, PAGE)] @@ -500,6 +624,45 @@ class UnwrapTest(unittest.TestCase): self.assertEqual(tls[5], 0) self.assertEqual(tls[2], 0) + def test_zero_size_segment_in_unbacked_load_range_is_accepted(self): + for offset in (0, 0x800, 0x1900, 0xFFFFFFFFFFFFFFFF): + for address in (0x4000, 0x4020): + with self.subTest(offset=offset, address=address): + segments = default_segments() + [(PT_LOAD, 6, offset, 0x4000, 0, PAGE), + (PT_TLS, 4, offset, address, 0, 8)] + container = bytearray(build_container(segments, default_records(), default_payloads())) + struct.pack_into("<Q", container, ELF_HEADER_OFFSET + PROGRAM_TABLE_OFFSET + 3 * 56 + 40, 0x2000) + struct.pack_into("<Q", container, ELF_HEADER_OFFSET + PROGRAM_TABLE_OFFSET + 4 * 56 + 40, 0x40) + result, output = self.convert(container) + self.assertEqual(result.returncode, 0, result.stderr) + _, headers = read_program_headers(output) + self.assertEqual(headers[4], (PT_TLS, 4, 0, address, address, 0, 0x40, 8)) + for address in (0x1088, 0x2000, 0x3000): + with self.subTest(bss_address=address): + segments = default_segments() + [(PT_TLS, 4, 0x1588, address, 0, 8)] + payloads = default_payloads() + payloads[0xA80] = pattern(23, 0x100) + container = bytearray(build_container(segments, default_records(), payloads)) + struct.pack_into("<Q", container, ELF_HEADER_OFFSET + PROGRAM_TABLE_OFFSET + 56 + 40, 0x2000) + struct.pack_into("<Q", container, ELF_HEADER_OFFSET + PROGRAM_TABLE_OFFSET + 3 * 56 + 40, 0x40) + result, output = self.convert(container) + self.assertEqual(result.returncode, 0, result.stderr) + _, headers = read_program_headers(output) + self.assertEqual(headers[3], (PT_TLS, 4, 0, address, address, 0, 0x40, 8)) + + def test_the_report_names_a_non_ascii_output_path_on_a_cp1252_console(self): + directory = Path(self.directory) / "\u65e5\u672c" + directory.mkdir() + source = directory / "input.self" + output = directory / "output.elf" + source.write_bytes(build_container(default_segments(), default_records(), default_payloads())) + env = {**os.environ, "PYTHONIOENCODING": "cp1252", "PYTHONUTF8": "0"} + result = subprocess.run([sys.executable, "-X", "utf8=0", str(Path(unwrap_self.__file__)), str(source), str(output)], + env=env, capture_output=True, timeout=30) + self.assertEqual(result.returncode, 0, result.stderr.decode("utf-8", errors="replace")) + self.assertIn(f"wrote {output}", result.stdout.decode("utf-8")) + self.assertTrue(output.exists()) + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_web_release.py b/tools/tests/test_web_release.py new file mode 100644 index 0000000000..5fbc00dc65 --- /dev/null +++ b/tools/tests/test_web_release.py @@ -0,0 +1,379 @@ +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_publication_replaces_obsolete_files_and_directory_conflicts(self): + self.seed_previous() + conflict = self.output / "native/relinker_wasm.mjs" + conflict.mkdir() + (conflict / "obsolete.txt").write_bytes(b"previous layout") + self.publish() + expected = {*FILES, "web-release.json", "release.json", ".nojekyll"} + manifest = json.loads((self.output / "release.json").read_text()) + expected.update(asset["path"] for asset in manifest["platforms"].values()) + self.assertEqual(set(self.snapshot()), expected) + for name, content in self.contents.items(): + self.assertEqual((self.output / name).read_bytes(), content) + + def test_failed_publication_restores_the_previous_site(self): + self.seed_previous() + previous = self.snapshot() + rename = Path.rename + + def failed_rename(path, destination): + if path != self.output and Path(destination) == self.output and path.name == "site": + raise PermissionError("publication blocked") + return rename(path, destination) + + with patch.object(Path, "rename", failed_rename): + with self.assertRaisesRegex(PermissionError, "publication blocked"): + self.publish() + self.assertEqual(self.snapshot(), previous) + self.assertEqual(list(self.build.glob(".web-release-*")), []) + + def test_failed_first_publication_leaves_no_output(self): + self.output.rmdir() + rename = Path.rename + + def failed_rename(path, destination): + if Path(destination) == self.output: + raise PermissionError("publication blocked") + return rename(path, destination) + + with patch.object(Path, "rename", failed_rename): + with self.assertRaisesRegex(PermissionError, "publication blocked"): + self.publish() + self.assertFalse(self.output.exists()) + self.assertEqual(list(self.build.glob(".web-release-*")), []) + + def test_output_file_is_rejected_without_replacing_it(self): + self.output.rmdir() + self.output.write_bytes(b"unrelated file") + with self.assertRaisesRegex(ValueError, "Release output is not a directory"): + self.publish() + self.assertEqual(self.output.read_bytes(), b"unrelated file") + self.assertEqual(list(self.build.glob(".web-release-*")), []) + + def test_failed_backup_keeps_the_previous_site(self): + self.seed_previous() + previous = self.snapshot() + rename = Path.rename + + def failed_rename(path, destination): + if path == self.output: + raise PermissionError("previous site is in use") + return rename(path, destination) + + with patch.object(Path, "rename", failed_rename): + with self.assertRaisesRegex(PermissionError, "previous site is in use"): + self.publish() + self.assertEqual(self.snapshot(), previous) + self.assertEqual(list(self.build.glob(".web-release-*")), []) + + def test_failed_restoration_retains_the_backup_and_reports_its_path(self): + self.seed_previous() + previous = self.snapshot() + rename = Path.rename + + def failed_rename(path, destination): + if path != self.output and Path(destination) == self.output: + raise PermissionError("destination is unavailable") + return rename(path, destination) + + with patch.object(Path, "rename", failed_rename): + with self.assertRaisesRegex(RuntimeError, "Previous site is preserved at") as error: + self.publish() + temporary, = self.build.glob(".web-release-*") + backup = temporary / "previous" + self.assertIn(str(backup), str(error.exception)) + self.assertEqual({path.relative_to(backup).as_posix(): path.read_bytes() + for path in backup.rglob("*") if path.is_file()}, previous) + self.assertFalse(self.output.exists()) + + 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 6c5a424c21..6d03d2200c 100644 --- a/tools/trace_digest.py +++ b/tools/trace_digest.py @@ -59,6 +59,17 @@ class Trace: entry[4] = any(is_wait(parent[2]) for parent in stack) stack.append(entry) + def select_threads(self, names): + available = self.threads.keys() | self.slices.keys() | self.folded.keys() + unknown = set(names) - {self.name(tid) for tid in available} + if unknown: + raise ValueError("unknown thread: " + ", ".join(sorted(unknown))) + selected = {tid for tid in available if self.name(tid) in names} + self.threads = {tid: name for tid, name in self.threads.items() if tid in selected} + self.slices = {tid: slices for tid, slices in self.slices.items() if tid in selected} + self.folded = {tid: value for tid, value in self.folded.items() if tid in selected} + self.batched = [entry for entry in self.batched if entry[0] in selected] + def name(self, tid): return self.threads.get(tid, f"tid {tid}") @@ -72,7 +83,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 +91,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 +103,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(" ") @@ -143,8 +154,8 @@ def digest(trace, summary=None, stacks=None, worst=5, top=10): leaders[name] += overlap if is_wait(name) and not nested: waited += overlap - if is_wait(name) and not nested: - waits.append((dur, name, trace.name(tid), ts)) + if is_wait(name) and not nested and (overlap > 0 or not frames): + waits.append((overlap, name, trace.name(tid), max(ts, frames[0][1]) if frames else ts)) threads.append({"thread": trace.name(tid), "busy_ms_per_frame": (traced - waited) / 1000.0 / count, "wait_ms_per_frame": waited / 1000.0 / count, "untraced_ms_per_frame": max(span_us - traced, 0.0) / 1000.0 / count, @@ -209,6 +220,9 @@ def table_cell(value): def markdown(result): lines = [] + if result.get("thread_filter"): + lines += ["Thread filter: " + ", ".join(table_cell(name) for name in result["thread_filter"]) + + ". Frame bounds, summary and stack samples remain capture-wide.", ""] summary = result["summary"] if summary: frame = summary["frame_ms"] @@ -264,7 +278,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] @@ -289,7 +303,7 @@ def digest_intervals(lines): "trace_dropped": line.get("trace_dropped", False)}) first = lines[0] if lines else {} keys = ("commit", "dirty", "env", "gpu", "driver", "os", "resolution") - changed = [index for index, line in enumerate(lines) if any(line.get(key) != first.get(key) for key in keys)] + changed = [index for index in range(1, len(lines)) if any(lines[index].get(key) != lines[index - 1].get(key) for key in keys)] return {"intervals": rows, "identity": {key: first.get(key) for key in keys}, "identity_changed_at": changed} @@ -328,12 +342,16 @@ def main(arguments=None): parser.add_argument("trace", help="aps5-trace-*.json.gz (or .json), or a .jsonl summary file with APS5_TRACE_SUMMARY_EVERY lines") parser.add_argument("--summary", help="file with the summary line (default: aps5-summary.jsonl next to the trace)") parser.add_argument("--stacks", help="collapsed-stack file of APS5_SAMPLE_WORKER (<file stem>.collapsed.txt)") + parser.add_argument("--thread", action="append", default=[], metavar="NAME", + help="include event tracks with this exact thread name; repeat to select several tracks") parser.add_argument("--frames", type=nonnegative_integer, default=5, help="slowest frames to list (default 5)") parser.add_argument("--top", type=nonnegative_integer, default=10, help="entries per list (default 10)") parser.add_argument("--json", action="store_true", help="print the digest as JSON") options = parser.parse_args(arguments) if options.summary and not Path(options.summary).is_file(): parser.error(f"summary file not found: {options.summary}") + if options.thread and options.trace.endswith(".jsonl"): + parser.error("--thread requires an event trace, not interval summaries") if options.trace.endswith(".jsonl"): result = digest_intervals(load_intervals(options.trace)) sys.stdout.write(json.dumps(result, indent=1) + "\n" if options.json else markdown_intervals(result)) @@ -342,15 +360,21 @@ def main(arguments=None): stacks = load_stacks(options.stacks) if options.stacks else None except (OSError, ValueError) as error: parser.error(str(error)) - result = digest(load_trace(options.trace), load_summary(options.summary, options.trace), + trace = load_trace(options.trace) + if options.thread: + trace.select_threads(options.thread) + result = digest(trace, load_summary(options.summary, options.trace), stacks, options.frames, options.top) + if options.thread: + result["thread_filter"] = sorted(set(options.thread)) sys.stdout.write(json.dumps(result, indent=1) + "\n" if options.json else markdown(result)) return 0 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 50d80bc51a..1ce81f2e12 100644 --- a/tools/unwrap_self.py +++ b/tools/unwrap_self.py @@ -95,12 +95,6 @@ def read_records(data): return records -def align_up(value, alignment): - if alignment <= 1: - return value - return (value + alignment - 1) // alignment * alignment - - def parse_elf(data): inner = data.find(b"\x7fELF") if inner < 0: @@ -108,6 +102,8 @@ def parse_elf(data): if inner + 0x40 > len(data): raise Refused("inner ELF header is truncated") header = bytearray(data[inner:inner + 0x40]) + if header[4] != 2 or header[5] != 1: + raise Refused("inner ELF is not 64-bit little-endian") program_offset = struct.unpack_from("<Q", header, 0x20)[0] entry_size = struct.unpack_from("<H", header, 0x36)[0] count = struct.unpack_from("<H", header, 0x38)[0] @@ -200,6 +196,9 @@ def place_segments(data, headers, records): if header["size"] and (offset > host["size"] or header["size"] > host["size"] - offset): raise Refused("segment %d does not fit the file-backed range of PT_LOAD %d" % (header["index"], host["index"])) + if host["size"] == 0 or offset > host["size"]: + header["source"] = 0 + continue header["host"] = host header["source"] = host["source"] + offset if tail is None or header["offset"] > tail[0]: @@ -250,10 +249,13 @@ def unwrap(path, output, verbose=False): check_decrypted(data, headers) regions = [] + load_regions = {} for header in headers: if header["type"] == PT_LOAD and header["size"]: - regions.append({"old": header["source"], "size": header["size"], - "vaddr": header["vaddr"], "align": header["align"]}) + region = {"old": header["source"], "size": header["size"], + "vaddr": header["vaddr"], "align": header["align"]} + regions.append(region) + load_regions[header["index"]] = region def region_for(offset, size): for region in regions: @@ -265,16 +267,15 @@ 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] cursor = max(FIRST_REGION, program_offset + len(headers) * 56) for region in regions: alignment = region["align"] if region["align"] and (region["align"] & (region["align"] - 1)) == 0 else 1 - placed = align_up(cursor, alignment) - if region["vaddr"] % alignment != placed % alignment: - placed += (region["vaddr"] % alignment - placed % alignment) % alignment + placed = cursor + (region["vaddr"] - cursor) % alignment region["new"] = placed cursor = placed + region["size"] @@ -294,7 +295,10 @@ def unwrap(path, output, verbose=False): if header["size"] == 0 and (header["type"] == PT_LOAD or header["source"] == 0): header["new"] = header["vaddr"] % max(header["align"], 1) if header["type"] == PT_LOAD else 0 continue - region = region_for(header["source"], header["size"]) + host = header if header["type"] == PT_LOAD else header["host"] + region = load_regions.get(host["index"]) if host is not None else None + if region is None: + region = region_for(header["source"], header["size"]) header["new"] = header["source"] if region is None else region["new"] + (header["source"] - region["old"]) if header["new"] + header["size"] > len(result): raise Refused("segment %d escaped the output" % header["index"]) @@ -343,4 +347,6 @@ def main(): if __name__ == "__main__": + if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8") sys.exit(main())